From ef180c489a891bfabae3361bb2a99b142e256085 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Fri, 4 Jan 2019 14:00:26 -0500 Subject: [PATCH 001/235] check config instead of config_entry for quirks flag (#19730) --- homeassistant/components/zha/__init__.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index 5ad06565b00..3fe8980c451 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -95,8 +95,7 @@ async def async_setup(hass, config): context={'source': config_entries.SOURCE_IMPORT}, data={ CONF_USB_PATH: conf[CONF_USB_PATH], - CONF_RADIO_TYPE: conf.get(CONF_RADIO_TYPE).value, - ENABLE_QUIRKS: conf[ENABLE_QUIRKS] + CONF_RADIO_TYPE: conf.get(CONF_RADIO_TYPE).value } )) return True @@ -107,17 +106,17 @@ async def async_setup_entry(hass, config_entry): Will automatically load components to support devices found on the network. """ - if config_entry.data.get(ENABLE_QUIRKS): - # needs to be done here so that the ZHA module is finished loading - # before zhaquirks is imported - # pylint: disable=W0611, W0612 - import zhaquirks # noqa - hass.data[DATA_ZHA] = hass.data.get(DATA_ZHA, {}) hass.data[DATA_ZHA][DATA_ZHA_DISPATCHERS] = [] config = hass.data[DATA_ZHA].get(DATA_ZHA_CONFIG, {}) + if config.get(ENABLE_QUIRKS, True): + # needs to be done here so that the ZHA module is finished loading + # before zhaquirks is imported + # pylint: disable=W0611, W0612 + import zhaquirks # noqa + usb_path = config_entry.data.get(CONF_USB_PATH) baudrate = config.get(CONF_BAUDRATE, DEFAULT_BAUDRATE) radio_type = config_entry.data.get(CONF_RADIO_TYPE) From bf40bea965638d7b1a9b6b64fb3f8702e74b1d0a Mon Sep 17 00:00:00 2001 From: Austin Drummond Date: Fri, 4 Jan 2019 14:54:37 -0500 Subject: [PATCH 002/235] Support for Homekit controller/alarm control panel (#19612) * added support for homekit security systems * fixed linting issues * fixed indentation issues * simplifired logic on homekit_controller alarm controller panel * cleaned up battery level const on homekit controller alarm control panel --- .../alarm_control_panel/homekit_controller.py | 117 ++++++++++++++++++ .../components/homekit_controller/__init__.py | 1 + 2 files changed, 118 insertions(+) create mode 100644 homeassistant/components/alarm_control_panel/homekit_controller.py diff --git a/homeassistant/components/alarm_control_panel/homekit_controller.py b/homeassistant/components/alarm_control_panel/homekit_controller.py new file mode 100644 index 00000000000..cc760a851cf --- /dev/null +++ b/homeassistant/components/alarm_control_panel/homekit_controller.py @@ -0,0 +1,117 @@ +""" +Support for Homekit Alarm Control Panel. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/alarm_control_panel.homekit_controller/ +""" +import logging + +from homeassistant.components.homekit_controller import (HomeKitEntity, + KNOWN_ACCESSORIES) +from homeassistant.components.alarm_control_panel import AlarmControlPanel +from homeassistant.const import ( + STATE_ALARM_DISARMED, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, + STATE_ALARM_ARMED_NIGHT, STATE_ALARM_TRIGGERED) +from homeassistant.const import ATTR_BATTERY_LEVEL + +DEPENDENCIES = ['homekit_controller'] + +ICON = 'mdi:security' + +_LOGGER = logging.getLogger(__name__) + +CURRENT_STATE_MAP = { + 0: STATE_ALARM_ARMED_HOME, + 1: STATE_ALARM_ARMED_AWAY, + 2: STATE_ALARM_ARMED_NIGHT, + 3: STATE_ALARM_DISARMED, + 4: STATE_ALARM_TRIGGERED +} + +TARGET_STATE_MAP = { + STATE_ALARM_ARMED_HOME: 0, + STATE_ALARM_ARMED_AWAY: 1, + STATE_ALARM_ARMED_NIGHT: 2, + STATE_ALARM_DISARMED: 3, +} + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up Homekit Alarm Control Panel support.""" + if discovery_info is None: + return + accessory = hass.data[KNOWN_ACCESSORIES][discovery_info['serial']] + add_entities([HomeKitAlarmControlPanel(accessory, discovery_info)], + True) + + +class HomeKitAlarmControlPanel(HomeKitEntity, AlarmControlPanel): + """Representation of a Homekit Alarm Control Panel.""" + + def __init__(self, *args): + """Initialise the Alarm Control Panel.""" + super().__init__(*args) + self._state = None + self._battery_level = None + + def update_characteristics(self, characteristics): + """Synchronise the Alarm Control Panel state with Home Assistant.""" + # pylint: disable=import-error + from homekit.model.characteristics import CharacteristicsTypes + + for characteristic in characteristics: + ctype = characteristic['type'] + ctype = CharacteristicsTypes.get_short(ctype) + if ctype == "security-system-state.current": + self._chars['security-system-state.current'] = \ + characteristic['iid'] + self._state = CURRENT_STATE_MAP[characteristic['value']] + elif ctype == "security-system-state.target": + self._chars['security-system-state.target'] = \ + characteristic['iid'] + elif ctype == "battery-level": + self._chars['battery-level'] = characteristic['iid'] + self._battery_level = characteristic['value'] + + @property + def icon(self): + """Return icon.""" + return ICON + + @property + def state(self): + """Return the state of the device.""" + return self._state + + def alarm_disarm(self, code=None): + """Send disarm command.""" + self.set_alarm_state(STATE_ALARM_DISARMED, code) + + def alarm_arm_away(self, code=None): + """Send arm command.""" + self.set_alarm_state(STATE_ALARM_ARMED_AWAY, code) + + def alarm_arm_home(self, code=None): + """Send stay command.""" + self.set_alarm_state(STATE_ALARM_ARMED_HOME, code) + + def alarm_arm_night(self, code=None): + """Send night command.""" + self.set_alarm_state(STATE_ALARM_ARMED_NIGHT, code) + + def set_alarm_state(self, state, code=None): + """Send state command.""" + characteristics = [{'aid': self._aid, + 'iid': self._chars['security-system-state.target'], + 'value': TARGET_STATE_MAP[state]}] + self.put_characteristics(characteristics) + + @property + def device_state_attributes(self): + """Return the optional state attributes.""" + if self._battery_level is None: + return None + + return { + ATTR_BATTERY_LEVEL: self._battery_level, + } diff --git a/homeassistant/components/homekit_controller/__init__.py b/homeassistant/components/homekit_controller/__init__.py index 09068773e4e..74fbb228f54 100644 --- a/homeassistant/components/homekit_controller/__init__.py +++ b/homeassistant/components/homekit_controller/__init__.py @@ -24,6 +24,7 @@ HOMEKIT_ACCESSORY_DISPATCH = { 'outlet': 'switch', 'switch': 'switch', 'thermostat': 'climate', + 'security-system': 'alarm_control_panel', } HOMEKIT_IGNORE = [ From 65c7bdc1ada3861f82ff49201b47f0091b381024 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Fri, 4 Jan 2019 22:02:42 +0100 Subject: [PATCH 003/235] Don't slugify unique id (#19770) --- homeassistant/components/xiaomi_aqara.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/xiaomi_aqara.py b/homeassistant/components/xiaomi_aqara.py index aa2102ca805..25e7a72db90 100644 --- a/homeassistant/components/xiaomi_aqara.py +++ b/homeassistant/components/xiaomi_aqara.py @@ -20,7 +20,6 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity 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.11.1'] @@ -222,11 +221,11 @@ class XiaomiDevice(Entity): if hasattr(self, '_data_key') \ and self._data_key: # pylint: disable=no-member - self._unique_id = slugify("{}-{}".format( + self._unique_id = "{}{}".format( self._data_key, # pylint: disable=no-member - self._sid)) + self._sid) else: - self._unique_id = slugify("{}-{}".format(self._type, self._sid)) + self._unique_id = "{}{}".format(self._type, self._sid) def _add_push_data_job(self, *args): self.hass.add_job(self.push_data, *args) From ed8f89df743e027aa6105601c3e7ea1dd728f7c9 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Fri, 4 Jan 2019 16:05:37 -0500 Subject: [PATCH 004/235] Use manufacturer id only for configure_reporting only when specified. (#19729) --- homeassistant/components/zha/helpers.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/zha/helpers.py b/homeassistant/components/zha/helpers.py index 426bceeb634..3212849f721 100644 --- a/homeassistant/components/zha/helpers.py +++ b/homeassistant/components/zha/helpers.py @@ -67,10 +67,13 @@ async def configure_reporting(entity_id, cluster, attr, skip_bind=False, attr_name = cluster.attributes.get(attr, [attr])[0] cluster_name = cluster.ep_attribute + kwargs = {} + if manufacturer: + kwargs['manufacturer'] = manufacturer try: res = await cluster.configure_reporting(attr, min_report, max_report, reportable_change, - manufacturer=manufacturer) + **kwargs) _LOGGER.debug( "%s: reporting '%s' attr on '%s' cluster: %d/%d/%d: Result: '%s'", entity_id, attr_name, cluster_name, min_report, max_report, From c7700ad11c18f3a317e854eca390151edae9f49b Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Fri, 4 Jan 2019 22:10:52 +0100 Subject: [PATCH 005/235] Fix some ESPHome race conditions (#19772) * Fix some ESPHome race conditions * Remove debug * Update requirements_all.txt * :ambulance: Fix IDE line length settings --- homeassistant/components/esphome/__init__.py | 51 +++++++++++-------- .../components/esphome/config_flow.py | 15 ++---- requirements_all.txt | 2 +- tests/components/esphome/test_config_flow.py | 42 ++------------- 4 files changed, 38 insertions(+), 72 deletions(-) diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index 04acba7c691..9034818a934 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -6,6 +6,7 @@ from typing import Any, Dict, List, Optional, TYPE_CHECKING, Callable import attr import voluptuous as vol +from homeassistant import const from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, \ EVENT_HOMEASSISTANT_STOP @@ -30,7 +31,7 @@ if TYPE_CHECKING: ServiceCall DOMAIN = 'esphome' -REQUIREMENTS = ['aioesphomeapi==1.3.0'] +REQUIREMENTS = ['aioesphomeapi==1.4.0'] DISPATCHER_UPDATE_ENTITY = 'esphome_{entry_id}_update_{component_key}_{key}' @@ -161,8 +162,8 @@ async def async_setup_entry(hass: HomeAssistantType, port = entry.data[CONF_PORT] password = entry.data[CONF_PASSWORD] - cli = APIClient(hass.loop, host, port, password) - await cli.start() + cli = APIClient(hass.loop, host, port, password, + client_info="Home Assistant {}".format(const.__version__)) # Store client in per-config-entry hass.data store = Store(hass, STORAGE_VERSION, STORAGE_KEY.format(entry.entry_id), @@ -181,8 +182,6 @@ async def async_setup_entry(hass: HomeAssistantType, hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, on_stop) ) - try_connect = await _setup_auto_reconnect_logic(hass, cli, entry, host) - @callback def async_on_state(state: 'EntityState') -> None: """Send dispatcher updates when a new state is received.""" @@ -247,7 +246,8 @@ async def async_setup_entry(hass: HomeAssistantType, # Re-connection logic will trigger after this await cli.disconnect() - cli.on_login = on_login + try_connect = await _setup_auto_reconnect_logic(hass, cli, entry, host, + on_login) # This is a bit of a hack: We schedule complete_setup into the # event loop and return immediately (return True) @@ -291,7 +291,7 @@ async def async_setup_entry(hass: HomeAssistantType, async def _setup_auto_reconnect_logic(hass: HomeAssistantType, cli: 'APIClient', - entry: ConfigEntry, host: str): + entry: ConfigEntry, host: str, on_login): """Set up the re-connect logic for the API client.""" from aioesphomeapi import APIConnectionError @@ -308,33 +308,40 @@ async def _setup_auto_reconnect_logic(hass: HomeAssistantType, data.available = False data.async_update_device_state(hass) - if tries != 0: - # If not first re-try, wait and print message - wait_time = min(2**tries, 300) - _LOGGER.info("Trying to reconnect in %s seconds", wait_time) - await asyncio.sleep(wait_time) - - if is_disconnect and tries == 0: + if is_disconnect: # This can happen often depending on WiFi signal strength. # So therefore all these connection warnings are logged # as infos. The "unavailable" logic will still trigger so the # user knows if the device is not connected. - _LOGGER.info("Disconnected from API") + _LOGGER.info("Disconnected from ESPHome API for %s", host) + + if tries != 0: + # If not first re-try, wait and print message + # Cap wait time at 1 minute. This is because while working on the + # device (e.g. soldering stuff), users don't want to have to wait + # a long time for their device to show up in HA again (this was + # mentioned a lot in early feedback) + # + # In the future another API will be set up so that the ESP can + # notify HA of connectivity directly, but for new we'll use a + # really short reconnect interval. + wait_time = int(round(min(1.8**tries, 60.0))) + _LOGGER.info("Trying to reconnect in %s seconds", wait_time) + await asyncio.sleep(wait_time) try: - await cli.connect() - await cli.login() + await cli.connect(on_stop=try_connect, login=True) except APIConnectionError as error: - _LOGGER.info("Can't connect to esphome API for '%s' (%s)", + _LOGGER.info("Can't connect to ESPHome API for %s: %s", host, error) # Schedule re-connect in event loop in order not to delay HA # startup. First connect is scheduled in tracked tasks. - data.reconnect_task = \ - hass.loop.create_task(try_connect(tries + 1, is_disconnect)) + data.reconnect_task = hass.loop.create_task( + try_connect(tries + 1, is_disconnect=False)) else: _LOGGER.info("Successfully connected to %s", host) + hass.async_create_task(on_login()) - cli.on_disconnect = try_connect return try_connect @@ -368,7 +375,7 @@ async def _cleanup_instance(hass: HomeAssistantType, disconnect_cb() for cleanup_callback in data.cleanup_callbacks: cleanup_callback() - await data.client.stop() + await data.client.disconnect() async def async_unload_entry(hass: HomeAssistantType, diff --git a/homeassistant/components/esphome/config_flow.py b/homeassistant/components/esphome/config_flow.py index 017cf8c8ee6..d6abf03cf5d 100644 --- a/homeassistant/components/esphome/config_flow.py +++ b/homeassistant/components/esphome/config_flow.py @@ -92,7 +92,6 @@ class EsphomeFlowHandler(config_entries.ConfigFlow): cli = APIClient(self.hass.loop, self._host, self._port, '') try: - await cli.start() await cli.connect() device_info = await cli.device_info() except APIConnectionError as err: @@ -100,7 +99,7 @@ class EsphomeFlowHandler(config_entries.ConfigFlow): return 'resolve_error', None return 'connection_error', None finally: - await cli.stop(force=True) + await cli.disconnect(force=True) return None, device_info @@ -111,17 +110,9 @@ class EsphomeFlowHandler(config_entries.ConfigFlow): cli = APIClient(self.hass.loop, self._host, self._port, self._password) try: - await cli.start() - await cli.connect() - except APIConnectionError: - await cli.stop(force=True) - return 'connection_error' - - try: - await cli.login() + await cli.connect(login=True) except APIConnectionError: + await cli.disconnect(force=True) return 'invalid_password' - finally: - await cli.stop(force=True) return None diff --git a/requirements_all.txt b/requirements_all.txt index 4242e0b38d1..4e924cd9e29 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -96,7 +96,7 @@ aioautomatic==0.6.5 aiodns==1.1.1 # homeassistant.components.esphome -aioesphomeapi==1.3.0 +aioesphomeapi==1.4.0 # homeassistant.components.freebox aiofreepybox==0.0.6 diff --git a/tests/components/esphome/test_config_flow.py b/tests/components/esphome/test_config_flow.py index d90db501a54..07398dce8be 100644 --- a/tests/components/esphome/test_config_flow.py +++ b/tests/components/esphome/test_config_flow.py @@ -31,10 +31,8 @@ def mock_client(): return mock_client mock_client.side_effect = mock_constructor - mock_client.start.return_value = mock_coro() mock_client.connect.return_value = mock_coro() - mock_client.stop.return_value = mock_coro() - mock_client.login.return_value = mock_coro() + mock_client.disconnect.return_value = mock_coro() yield mock_client @@ -69,10 +67,9 @@ async def test_user_connection_works(hass, mock_client): 'password': '' } assert result['title'] == 'test' - assert len(mock_client.start.mock_calls) == 1 assert len(mock_client.connect.mock_calls) == 1 assert len(mock_client.device_info.mock_calls) == 1 - assert len(mock_client.stop.mock_calls) == 1 + assert len(mock_client.disconnect.mock_calls) == 1 assert mock_client.host == '127.0.0.1' assert mock_client.port == 80 assert mock_client.password == '' @@ -106,10 +103,9 @@ async def test_user_resolve_error(hass, mock_api_connection_error, assert result['errors'] == { 'base': 'resolve_error' } - assert len(mock_client.start.mock_calls) == 1 assert len(mock_client.connect.mock_calls) == 1 assert len(mock_client.device_info.mock_calls) == 1 - assert len(mock_client.stop.mock_calls) == 1 + assert len(mock_client.disconnect.mock_calls) == 1 async def test_user_connection_error(hass, mock_api_connection_error, @@ -131,10 +127,9 @@ async def test_user_connection_error(hass, mock_api_connection_error, assert result['errors'] == { 'base': 'connection_error' } - assert len(mock_client.start.mock_calls) == 1 assert len(mock_client.connect.mock_calls) == 1 assert len(mock_client.device_info.mock_calls) == 1 - assert len(mock_client.stop.mock_calls) == 1 + assert len(mock_client.disconnect.mock_calls) == 1 async def test_user_with_password(hass, mock_client): @@ -176,12 +171,12 @@ async def test_user_invalid_password(hass, mock_api_connection_error, mock_client.device_info.return_value = mock_coro( MockDeviceInfo(True, "test")) - mock_client.login.side_effect = mock_api_connection_error await flow.async_step_user(user_input={ 'host': '127.0.0.1', 'port': 6053, }) + mock_client.connect.side_effect = mock_api_connection_error result = await flow.async_step_authenticate(user_input={ 'password': 'invalid' }) @@ -191,30 +186,3 @@ async def test_user_invalid_password(hass, mock_api_connection_error, assert result['errors'] == { 'base': 'invalid_password' } - - -async def test_user_login_connection_error(hass, mock_api_connection_error, - mock_client): - """Test user step with connection error during login phase.""" - flow = config_flow.EsphomeFlowHandler() - flow.hass = hass - await flow.async_step_user(user_input=None) - - mock_client.device_info.return_value = mock_coro( - MockDeviceInfo(True, "test")) - - await flow.async_step_user(user_input={ - 'host': '127.0.0.1', - 'port': 6053, - }) - - mock_client.connect.side_effect = mock_api_connection_error - result = await flow.async_step_authenticate(user_input={ - 'password': 'invalid' - }) - - assert result['type'] == 'form' - assert result['step_id'] == 'authenticate' - assert result['errors'] == { - 'base': 'connection_error' - } From a1cb4018a10b23cb7de5b30fc0e0bc54dda850d2 Mon Sep 17 00:00:00 2001 From: sander76 Date: Fri, 4 Jan 2019 22:19:06 +0100 Subject: [PATCH 006/235] update powerview scene component to latest api. (#19717) --- .../scene/hunterdouglas_powerview.py | 21 ++++++++++--------- requirements_all.txt | 2 +- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/scene/hunterdouglas_powerview.py b/homeassistant/components/scene/hunterdouglas_powerview.py index 8c1ffa17e90..7676deb1a9c 100644 --- a/homeassistant/components/scene/hunterdouglas_powerview.py +++ b/homeassistant/components/scene/hunterdouglas_powerview.py @@ -8,14 +8,14 @@ import logging import voluptuous as vol +import homeassistant.helpers.config_validation as cv from homeassistant.components.scene import Scene, DOMAIN from homeassistant.const import CONF_PLATFORM from homeassistant.helpers.aiohttp_client import async_get_clientsession -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import async_generate_entity_id _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['aiopvapi==1.5.4'] +REQUIREMENTS = ['aiopvapi==1.6.14'] ENTITY_ID_FORMAT = DOMAIN + '.{}' HUB_ADDRESS = 'address' @@ -25,6 +25,7 @@ PLATFORM_SCHEMA = vol.Schema({ vol.Required(HUB_ADDRESS): cv.string, }) + SCENE_DATA = 'sceneData' ROOM_DATA = 'roomData' SCENE_NAME = 'name' @@ -39,6 +40,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up home assistant scene entries.""" # from aiopvapi.hub import Hub + from aiopvapi.helpers.aiorequest import AioRequest from aiopvapi.scenes import Scenes from aiopvapi.rooms import Rooms from aiopvapi.resources.scene import Scene as PvScene @@ -46,18 +48,17 @@ async def async_setup_platform(hass, config, async_add_entities, hub_address = config.get(HUB_ADDRESS) websession = async_get_clientsession(hass) - _scenes = await Scenes( - hub_address, hass.loop, websession).get_resources() - _rooms = await Rooms( - hub_address, hass.loop, websession).get_resources() + pv_request = AioRequest(hub_address, loop=hass.loop, websession=websession) + + _scenes = await Scenes(pv_request).get_resources() + _rooms = await Rooms(pv_request).get_resources() if not _scenes or not _rooms: _LOGGER.error( "Unable to initialize PowerView hub: %s", hub_address) return pvscenes = (PowerViewScene(hass, - PvScene(_raw_scene, hub_address, hass.loop, - websession), _rooms) + PvScene(_raw_scene, pv_request), _rooms) for _raw_scene in _scenes[SCENE_DATA]) async_add_entities(pvscenes) @@ -96,6 +97,6 @@ class PowerViewScene(Scene): """Icon to use in the frontend.""" return 'mdi:blinds' - def async_activate(self): + async def async_activate(self): """Activate scene. Try to get entities into requested state.""" - yield from self._scene.activate() + await self._scene.activate() diff --git a/requirements_all.txt b/requirements_all.txt index 4e924cd9e29..588ae10062f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -124,7 +124,7 @@ aiolifx==0.6.7 aiolifx_effects==0.2.1 # homeassistant.components.scene.hunterdouglas_powerview -aiopvapi==1.5.4 +aiopvapi==1.6.14 # homeassistant.components.unifi aiounifi==3 From bf29824dac0af0bd5c2f9e8d315f0f0ac2927a00 Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Fri, 4 Jan 2019 22:37:42 +0100 Subject: [PATCH 007/235] Update HAP-python to 2.4.2 (#19776) * Bugfixes for connection issues --- homeassistant/components/homekit/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index c8aea5f8fb3..c34b527252f 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -30,7 +30,7 @@ from .const import ( from .util import ( show_setup_message, validate_entity_config, validate_media_player_features) -REQUIREMENTS = ['HAP-python==2.4.1'] +REQUIREMENTS = ['HAP-python==2.4.2'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 588ae10062f..a904d27d1ba 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -33,7 +33,7 @@ Adafruit-SHT31==1.0.2 # Adafruit_BBIO==1.0.0 # homeassistant.components.homekit -HAP-python==2.4.1 +HAP-python==2.4.2 # homeassistant.components.notify.mastodon Mastodon.py==1.3.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1982d1cc136..98e8cd54b7a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -19,7 +19,7 @@ requests_mock==1.5.2 # homeassistant.components.homekit -HAP-python==2.4.1 +HAP-python==2.4.2 # homeassistant.components.sensor.rmvtransport PyRMVtransport==0.1.3 From aacf7ba9aa46839136444d6e9579618799325195 Mon Sep 17 00:00:00 2001 From: keesak <15312751+cdkonecny@users.noreply.github.com> Date: Sat, 5 Jan 2019 02:48:40 -0600 Subject: [PATCH 008/235] Add support for Kwikset 914 Convert - lock.zwave id0446 (#19710) (#19722) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you 👍 --- homeassistant/components/lock/zwave.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/lock/zwave.py b/homeassistant/components/lock/zwave.py index b4bb233c9cc..49f5d4c74e9 100644 --- a/homeassistant/components/lock/zwave.py +++ b/homeassistant/components/lock/zwave.py @@ -37,6 +37,7 @@ DEVICE_MAPPINGS = { POLYCONTROL_DANALOCK_V2_BTZE_LOCK: WORKAROUND_V2BTZE, # Kwikset 914TRL ZW500 (0x0090, 0x440): WORKAROUND_DEVICE_STATE, + (0x0090, 0x446): WORKAROUND_DEVICE_STATE, # Yale YRD210 (0x0129, 0x0209): WORKAROUND_DEVICE_STATE, (0x0129, 0xAA00): WORKAROUND_DEVICE_STATE, From fb5b5223fb082ccd6da6fab32ca754cd32bdcb56 Mon Sep 17 00:00:00 2001 From: Austin Drummond Date: Sat, 5 Jan 2019 03:59:43 -0500 Subject: [PATCH 009/235] Added zwave lock state from alarm type workaround (#18996) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you 👍 * added zwave lock state from alarm type workaround * fixed test indents * more linting fixes * one more linting fix * simplified logic * fixed lint new lines * fixed merge conflict issue * fixed definition of _alarm_type_workaround in zwave lock --- homeassistant/components/lock/zwave.py | 14 ++++++++- tests/components/lock/test_zwave.py | 41 ++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/lock/zwave.py b/homeassistant/components/lock/zwave.py index 49f5d4c74e9..77afe688c2e 100644 --- a/homeassistant/components/lock/zwave.py +++ b/homeassistant/components/lock/zwave.py @@ -32,6 +32,7 @@ POLYCONTROL_DANALOCK_V2_BTZE_LOCK = (POLYCONTROL, DANALOCK_V2_BTZE) WORKAROUND_V2BTZE = 1 WORKAROUND_DEVICE_STATE = 2 WORKAROUND_TRACK_MESSAGE = 4 +WORKAROUND_ALARM_TYPE = 8 DEVICE_MAPPINGS = { POLYCONTROL_DANALOCK_V2_BTZE_LOCK: WORKAROUND_V2BTZE, @@ -43,7 +44,7 @@ DEVICE_MAPPINGS = { (0x0129, 0xAA00): WORKAROUND_DEVICE_STATE, (0x0129, 0x0000): WORKAROUND_DEVICE_STATE, # Yale YRD220 (as reported by adrum in PR #17386) - (0x0109, 0x0000): WORKAROUND_DEVICE_STATE, + (0x0109, 0x0000): WORKAROUND_DEVICE_STATE | WORKAROUND_ALARM_TYPE, # Schlage BE469 (0x003B, 0x5044): WORKAROUND_DEVICE_STATE | WORKAROUND_TRACK_MESSAGE, # Schlage FE599NX @@ -237,6 +238,7 @@ class ZwaveLock(zwave.ZWaveDeviceEntity, LockDevice): self._state_workaround = False self._track_message_workaround = False self._previous_message = None + self._alarm_type_workaround = False # Enable appropriate workaround flags for our device # Make sure that we have values for the key before converting to int @@ -257,6 +259,10 @@ class ZwaveLock(zwave.ZWaveDeviceEntity, LockDevice): if workaround & WORKAROUND_TRACK_MESSAGE: self._track_message_workaround = True _LOGGER.debug("Message tracking workaround enabled") + if workaround & WORKAROUND_ALARM_TYPE: + self._alarm_type_workaround = True + _LOGGER.debug( + "Alarm Type device state workaround enabled") self.update_properties() def update_properties(self): @@ -311,6 +317,12 @@ class ZwaveLock(zwave.ZWaveDeviceEntity, LockDevice): if not alarm_type: return + + if self._alarm_type_workaround: + self._state = LOCK_STATUS.get(str(alarm_type)) + _LOGGER.debug("workaround: lock state set to %s -- alarm type: %s", + self._state, str(alarm_type)) + if alarm_type == 21: self._lock_status = '{}{}'.format( LOCK_ALARM_TYPE.get(str(alarm_type)), diff --git a/tests/components/lock/test_zwave.py b/tests/components/lock/test_zwave.py index 484e4796759..07095e4fe3e 100644 --- a/tests/components/lock/test_zwave.py +++ b/tests/components/lock/test_zwave.py @@ -143,6 +143,47 @@ def test_v2btze_value_changed(mock_openzwave): assert device.is_locked +def test_alarm_type_workaround(mock_openzwave): + """Test value changed for Z-Wave lock using alarm type.""" + node = MockNode(manufacturer_id='0109', product_id='0000') + values = MockEntityValues( + primary=MockValue(data=True, node=node), + access_control=None, + alarm_type=MockValue(data=16, node=node), + alarm_level=None, + ) + device = zwave.get_device(node=node, values=values) + assert not device.is_locked + + values.alarm_type.data = 18 + value_changed(values.alarm_type) + assert device.is_locked + + values.alarm_type.data = 19 + value_changed(values.alarm_type) + assert not device.is_locked + + values.alarm_type.data = 21 + value_changed(values.alarm_type) + assert device.is_locked + + values.alarm_type.data = 22 + value_changed(values.alarm_type) + assert not device.is_locked + + values.alarm_type.data = 24 + value_changed(values.alarm_type) + assert device.is_locked + + values.alarm_type.data = 25 + value_changed(values.alarm_type) + assert not device.is_locked + + values.alarm_type.data = 27 + value_changed(values.alarm_type) + assert device.is_locked + + def test_lock_access_control(mock_openzwave): """Test access control for Z-Wave lock.""" node = MockNode() From 0125b3fd80e56e1f67edcfaed185ac0340dc03fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren=20Oldag?= Date: Sat, 5 Jan 2019 14:05:37 +0100 Subject: [PATCH 010/235] Upgrade pwmled to 1.4.0 (#19783) --- homeassistant/components/light/rpi_gpio_pwm.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/light/rpi_gpio_pwm.py b/homeassistant/components/light/rpi_gpio_pwm.py index 25c72c247ee..a09666e89cd 100644 --- a/homeassistant/components/light/rpi_gpio_pwm.py +++ b/homeassistant/components/light/rpi_gpio_pwm.py @@ -15,7 +15,7 @@ from homeassistant.components.light import ( import homeassistant.helpers.config_validation as cv import homeassistant.util.color as color_util -REQUIREMENTS = ['pwmled==1.3.0'] +REQUIREMENTS = ['pwmled==1.4.0'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index a904d27d1ba..6a0eb4db397 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -839,7 +839,7 @@ pushbullet.py==0.11.0 pushetta==1.0.15 # homeassistant.components.light.rpi_gpio_pwm -pwmled==1.3.0 +pwmled==1.4.0 # homeassistant.components.august py-august==0.7.0 From 68723730a7bf8dfaee19c8d46e22258539c52fbd Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Sat, 5 Jan 2019 16:00:07 +0100 Subject: [PATCH 011/235] Add ESPHome native API discovery (#19399) * ESPHome discovery * Add note about netdisco * :abcd: * Address comments * Bump netdisco to 2.3.0 * Update requirements_all.txt --- homeassistant/components/discovery.py | 3 +- .../components/esphome/config_flow.py | 18 +++++ requirements_all.txt | 2 +- tests/components/esphome/test_config_flow.py | 65 ++++++++++++++++++- 4 files changed, 84 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/discovery.py b/homeassistant/components/discovery.py index 5d8102e3056..f87395520bb 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==2.2.0'] +REQUIREMENTS = ['netdisco==2.3.0'] DOMAIN = 'discovery' @@ -51,6 +51,7 @@ SERVICE_DLNA_DMR = 'dlna_dmr' CONFIG_ENTRY_HANDLERS = { SERVICE_DAIKIN: 'daikin', SERVICE_DECONZ: 'deconz', + 'esphome': 'esphome', 'google_cast': 'cast', SERVICE_HUE: 'hue', SERVICE_TELLDUSLIVE: 'tellduslive', diff --git a/homeassistant/components/esphome/config_flow.py b/homeassistant/components/esphome/config_flow.py index d6abf03cf5d..1f71d8d66b5 100644 --- a/homeassistant/components/esphome/config_flow.py +++ b/homeassistant/components/esphome/config_flow.py @@ -53,6 +53,24 @@ class EsphomeFlowHandler(config_entries.ConfigFlow): errors=errors ) + async def async_step_discovery(self, user_input: ConfigType): + """Handle discovery.""" + # mDNS hostname has additional '.' at end + hostname = user_input['hostname'][:-1] + hosts = (hostname, user_input['host']) + for entry in self._async_current_entries(): + if entry.data['host'] in hosts: + return self.async_abort( + reason='already_configured' + ) + + # Prefer .local addresses (mDNS is available after all, otherwise + # we wouldn't have received the discovery message) + return await self.async_step_user(user_input={ + 'host': hostname, + 'port': user_input['port'], + }) + def _async_get_entry(self): return self.async_create_entry( title=self._name, diff --git a/requirements_all.txt b/requirements_all.txt index 6a0eb4db397..fc8240c263b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -705,7 +705,7 @@ nessclient==0.9.9 netdata==0.1.2 # homeassistant.components.discovery -netdisco==2.2.0 +netdisco==2.3.0 # homeassistant.components.sensor.neurio_energy neurio==0.3.1 diff --git a/tests/components/esphome/test_config_flow.py b/tests/components/esphome/test_config_flow.py index 07398dce8be..1291aa53123 100644 --- a/tests/components/esphome/test_config_flow.py +++ b/tests/components/esphome/test_config_flow.py @@ -1,11 +1,11 @@ """Test config flow.""" from collections import namedtuple -from unittest.mock import patch, MagicMock +from unittest.mock import MagicMock, patch import pytest from homeassistant.components.esphome import config_flow -from tests.common import mock_coro +from tests.common import mock_coro, MockConfigEntry MockDeviceInfo = namedtuple("DeviceInfo", ["uses_password", "name"]) @@ -186,3 +186,64 @@ async def test_user_invalid_password(hass, mock_api_connection_error, assert result['errors'] == { 'base': 'invalid_password' } + + +async def test_discovery_initiation(hass, mock_client): + """Test discovery importing works.""" + flow = config_flow.EsphomeFlowHandler() + flow.hass = hass + service_info = { + 'host': '192.168.43.183', + 'port': 6053, + 'hostname': 'test8266.local.', + 'properties': {} + } + + mock_client.device_info.return_value = mock_coro( + MockDeviceInfo(False, "test8266")) + + result = await flow.async_step_discovery(user_input=service_info) + assert result['type'] == 'create_entry' + assert result['title'] == 'test8266' + assert result['data']['host'] == 'test8266.local' + assert result['data']['port'] == 6053 + + +async def test_discovery_already_configured_hostname(hass, mock_client): + """Test discovery aborts if already configured via hostname.""" + MockConfigEntry( + domain='esphome', + data={'host': 'test8266.local', 'port': 6053, 'password': ''} + ).add_to_hass(hass) + + flow = config_flow.EsphomeFlowHandler() + flow.hass = hass + service_info = { + 'host': '192.168.43.183', + 'port': 6053, + 'hostname': 'test8266.local.', + 'properties': {} + } + result = await flow.async_step_discovery(user_input=service_info) + assert result['type'] == 'abort' + assert result['reason'] == 'already_configured' + + +async def test_discovery_already_configured_ip(hass, mock_client): + """Test discovery aborts if already configured via static IP.""" + MockConfigEntry( + domain='esphome', + data={'host': '192.168.43.183', 'port': 6053, 'password': ''} + ).add_to_hass(hass) + + flow = config_flow.EsphomeFlowHandler() + flow.hass = hass + service_info = { + 'host': '192.168.43.183', + 'port': 6053, + 'hostname': 'test8266.local.', + 'properties': {} + } + result = await flow.async_step_discovery(user_input=service_info) + assert result['type'] == 'abort' + assert result['reason'] == 'already_configured' From 3a5ba77e041b7a70483f98beb77fcf89691e573a Mon Sep 17 00:00:00 2001 From: Eliseo Martelli Date: Sat, 5 Jan 2019 17:42:36 +0100 Subject: [PATCH 012/235] Rename air pollutants to air quality (#19448) * mv component folder * moved in airquality * changed names in files * renamed test init * renamed test air quality * renamed in tests * renamed coverage * fixed naming * corrected attr names * changed attr names --- .coveragerc | 2 +- .../__init__.py | 60 +++++++++---------- .../{air_pollutants => air_quality}/demo.py | 20 +++---- .../opensensemap.py | 20 +++---- homeassistant/components/demo.py | 2 +- requirements_all.txt | 2 +- tests/components/air_pollutants/__init__.py | 1 - .../air_pollutants/test_air_pollutants.py | 42 ------------- tests/components/air_quality/__init__.py | 1 + .../air_quality/test_air_quality.py | 42 +++++++++++++ 10 files changed, 96 insertions(+), 96 deletions(-) rename homeassistant/components/{air_pollutants => air_quality}/__init__.py (66%) rename homeassistant/components/{air_pollutants => air_quality}/demo.py (66%) rename homeassistant/components/{air_pollutants => air_quality}/opensensemap.py (80%) delete mode 100644 tests/components/air_pollutants/__init__.py delete mode 100644 tests/components/air_pollutants/test_air_pollutants.py create mode 100644 tests/components/air_quality/__init__.py create mode 100644 tests/components/air_quality/test_air_quality.py diff --git a/.coveragerc b/.coveragerc index 2be86802a85..3f8d0e6959b 100644 --- a/.coveragerc +++ b/.coveragerc @@ -442,7 +442,7 @@ omit = homeassistant/components/spider.py homeassistant/components/*/spider.py - homeassistant/components/air_pollutants/opensensemap.py + homeassistant/components/air_quality/opensensemap.py homeassistant/components/alarm_control_panel/alarmdotcom.py homeassistant/components/alarm_control_panel/canary.py homeassistant/components/alarm_control_panel/concord232.py diff --git a/homeassistant/components/air_pollutants/__init__.py b/homeassistant/components/air_quality/__init__.py similarity index 66% rename from homeassistant/components/air_pollutants/__init__.py rename to homeassistant/components/air_quality/__init__.py index f810807242c..7aed61ee11c 100644 --- a/homeassistant/components/air_pollutants/__init__.py +++ b/homeassistant/components/air_quality/__init__.py @@ -1,8 +1,8 @@ """ -Component for handling Air Pollutants data for your location. +Component for handling Air Quality data for your location. For more details about this component, please refer to the documentation at -https://home-assistant.io/components/air_pollutants/ +https://home-assistant.io/components/air_quality/ """ from datetime import timedelta import logging @@ -13,43 +13,43 @@ from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) -ATTR_AIR_POLLUTANTS_AQI = 'air_quality_index' -ATTR_AIR_POLLUTANTS_ATTRIBUTION = 'attribution' -ATTR_AIR_POLLUTANTS_C02 = 'carbon_dioxide' -ATTR_AIR_POLLUTANTS_CO = 'carbon_monoxide' -ATTR_AIR_POLLUTANTS_N2O = 'nitrogen_oxide' -ATTR_AIR_POLLUTANTS_NO = 'nitrogen_monoxide' -ATTR_AIR_POLLUTANTS_NO2 = 'nitrogen_dioxide' -ATTR_AIR_POLLUTANTS_OZONE = 'ozone' -ATTR_AIR_POLLUTANTS_PM_0_1 = 'particulate_matter_0_1' -ATTR_AIR_POLLUTANTS_PM_10 = 'particulate_matter_10' -ATTR_AIR_POLLUTANTS_PM_2_5 = 'particulate_matter_2_5' -ATTR_AIR_POLLUTANTS_SO2 = 'sulphur_dioxide' +ATTR_AQI = 'air_quality_index' +ATTR_ATTRIBUTION = 'attribution' +ATTR_C02 = 'carbon_dioxide' +ATTR_CO = 'carbon_monoxide' +ATTR_N2O = 'nitrogen_oxide' +ATTR_NO = 'nitrogen_monoxide' +ATTR_NO2 = 'nitrogen_dioxide' +ATTR_OZONE = 'ozone' +ATTR_PM_0_1 = 'particulate_matter_0_1' +ATTR_PM_10 = 'particulate_matter_10' +ATTR_PM_2_5 = 'particulate_matter_2_5' +ATTR_SO2 = 'sulphur_dioxide' -DOMAIN = 'air_pollutants' +DOMAIN = 'air_quality' ENTITY_ID_FORMAT = DOMAIN + '.{}' SCAN_INTERVAL = timedelta(seconds=30) PROP_TO_ATTR = { - 'air_quality_index': ATTR_AIR_POLLUTANTS_AQI, - 'attribution': ATTR_AIR_POLLUTANTS_ATTRIBUTION, - 'carbon_dioxide': ATTR_AIR_POLLUTANTS_C02, - 'carbon_monoxide': ATTR_AIR_POLLUTANTS_CO, - 'nitrogen_oxide': ATTR_AIR_POLLUTANTS_N2O, - 'nitrogen_monoxide': ATTR_AIR_POLLUTANTS_NO, - 'nitrogen_dioxide': ATTR_AIR_POLLUTANTS_NO2, - 'ozone': ATTR_AIR_POLLUTANTS_OZONE, - 'particulate_matter_0_1': ATTR_AIR_POLLUTANTS_PM_0_1, - 'particulate_matter_10': ATTR_AIR_POLLUTANTS_PM_10, - 'particulate_matter_2_5': ATTR_AIR_POLLUTANTS_PM_2_5, - 'sulphur_dioxide': ATTR_AIR_POLLUTANTS_SO2, + 'air_quality_index': ATTR_AQI, + 'attribution': ATTR_ATTRIBUTION, + 'carbon_dioxide': ATTR_C02, + 'carbon_monoxide': ATTR_CO, + 'nitrogen_oxide': ATTR_N2O, + 'nitrogen_monoxide': ATTR_NO, + 'nitrogen_dioxide': ATTR_NO2, + 'ozone': ATTR_OZONE, + 'particulate_matter_0_1': ATTR_PM_0_1, + 'particulate_matter_10': ATTR_PM_10, + 'particulate_matter_2_5': ATTR_PM_2_5, + 'sulphur_dioxide': ATTR_SO2, } async def async_setup(hass, config): - """Set up the air pollutants component.""" + """Set up the air quality component.""" component = hass.data[DOMAIN] = EntityComponent( _LOGGER, DOMAIN, hass, SCAN_INTERVAL) await component.async_setup(config) @@ -66,8 +66,8 @@ async def async_unload_entry(hass, entry): return await hass.data[DOMAIN].async_unload_entry(entry) -class AirPollutantsEntity(Entity): - """ABC for air pollutants data.""" +class AirQualityEntity(Entity): + """ABC for air quality data.""" @property def particulate_matter_2_5(self): diff --git a/homeassistant/components/air_pollutants/demo.py b/homeassistant/components/air_quality/demo.py similarity index 66% rename from homeassistant/components/air_pollutants/demo.py rename to homeassistant/components/air_quality/demo.py index 06c407d8608..b2b9c10574f 100644 --- a/homeassistant/components/air_pollutants/demo.py +++ b/homeassistant/components/air_quality/demo.py @@ -1,25 +1,25 @@ """ -Demo platform that offers fake air pollutants data. +Demo platform that offers fake air quality data. For more details about this platform, please refer to the documentation https://home-assistant.io/components/demo/ """ -from homeassistant.components.air_pollutants import AirPollutantsEntity +from homeassistant.components.air_quality import AirQualityEntity def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Air Pollutants.""" + """Set up the Air Quality.""" add_entities([ - DemoAirPollutants('Home', 14, 23, 100), - DemoAirPollutants('Office', 4, 16, None) + DemoAirQuality('Home', 14, 23, 100), + DemoAirQuality('Office', 4, 16, None) ]) -class DemoAirPollutants(AirPollutantsEntity): - """Representation of Air Pollutants data.""" +class DemoAirQuality(AirQualityEntity): + """Representation of Air Quality data.""" def __init__(self, name, pm_2_5, pm_10, n2o): - """Initialize the Demo Air Pollutants.""" + """Initialize the Demo Air Quality.""" self._name = name self._pm_2_5 = pm_2_5 self._pm_10 = pm_10 @@ -28,11 +28,11 @@ class DemoAirPollutants(AirPollutantsEntity): @property def name(self): """Return the name of the sensor.""" - return '{} {}'.format('Demo Air Pollutants', self._name) + return '{} {}'.format('Demo Air Quality', self._name) @property def should_poll(self): - """No polling needed for Demo Air Pollutants.""" + """No polling needed for Demo Air Quality.""" return False @property diff --git a/homeassistant/components/air_pollutants/opensensemap.py b/homeassistant/components/air_quality/opensensemap.py similarity index 80% rename from homeassistant/components/air_pollutants/opensensemap.py rename to homeassistant/components/air_quality/opensensemap.py index ae4625bbbe9..fe3cca4876e 100644 --- a/homeassistant/components/air_pollutants/opensensemap.py +++ b/homeassistant/components/air_quality/opensensemap.py @@ -1,16 +1,16 @@ """ -Support for openSenseMap Air Pollutants data. +Support for openSenseMap Air Quality data. For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/air_pollutants_opensensemap/ +https://home-assistant.io/components/air_quality/opensensemap/ """ from datetime import timedelta import logging import voluptuous as vol -from homeassistant.components.air_pollutants import ( - PLATFORM_SCHEMA, AirPollutantsEntity) +from homeassistant.components.air_quality import ( + PLATFORM_SCHEMA, AirQualityEntity) from homeassistant.const import CONF_NAME from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv @@ -34,7 +34,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ async def async_setup_platform( hass, config, async_add_entities, discovery_info=None): - """Set up the openSenseMap air pollutants platform.""" + """Set up the openSenseMap air quality platform.""" from opensensemap_api import OpenSenseMap name = config.get(CONF_NAME) @@ -51,20 +51,20 @@ async def async_setup_platform( station_name = osm_api.api.data['name'] if name is None else name - async_add_entities([OpenSenseMapPollutants(station_name, osm_api)], True) + async_add_entities([OpenSenseMapQuality(station_name, osm_api)], True) -class OpenSenseMapPollutants(AirPollutantsEntity): - """Implementation of an openSenseMap air pollutants entity.""" +class OpenSenseMapQuality(AirQualityEntity): + """Implementation of an openSenseMap air quality entity.""" def __init__(self, name, osm): - """Initialize the air pollutants entity.""" + """Initialize the air quality entity.""" self._name = name self._osm = osm @property def name(self): - """Return the name of the air pollutants entity.""" + """Return the name of the air quality entity.""" return self._name @property diff --git a/homeassistant/components/demo.py b/homeassistant/components/demo.py index d1bca45400b..2b9854fbcc7 100644 --- a/homeassistant/components/demo.py +++ b/homeassistant/components/demo.py @@ -15,7 +15,7 @@ DEPENDENCIES = ['conversation', 'introduction', 'zone'] DOMAIN = 'demo' COMPONENTS_WITH_DEMO_PLATFORM = [ - 'air_pollutants', + 'air_quality', 'alarm_control_panel', 'binary_sensor', 'calendar', diff --git a/requirements_all.txt b/requirements_all.txt index fc8240c263b..116713e247c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -746,7 +746,7 @@ openevsewifi==0.4 # homeassistant.components.media_player.openhome openhomedevice==0.4.2 -# homeassistant.components.air_pollutants.opensensemap +# homeassistant.components.air_quality.opensensemap opensensemap-api==0.1.3 # homeassistant.components.switch.orvibo diff --git a/tests/components/air_pollutants/__init__.py b/tests/components/air_pollutants/__init__.py deleted file mode 100644 index 98af2395a1f..00000000000 --- a/tests/components/air_pollutants/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The tests for Air Pollutants platforms.""" diff --git a/tests/components/air_pollutants/test_air_pollutants.py b/tests/components/air_pollutants/test_air_pollutants.py deleted file mode 100644 index bbbd85b3a0c..00000000000 --- a/tests/components/air_pollutants/test_air_pollutants.py +++ /dev/null @@ -1,42 +0,0 @@ -"""The tests for the Air Pollutants component.""" -from homeassistant.components.air_pollutants import ( - ATTR_AIR_POLLUTANTS_ATTRIBUTION, ATTR_AIR_POLLUTANTS_N2O, - ATTR_AIR_POLLUTANTS_OZONE, ATTR_AIR_POLLUTANTS_PM_10) -from homeassistant.setup import async_setup_component - - -async def test_state(hass): - """Test Air Pollutants state.""" - config = { - 'air_pollutants': { - 'platform': 'demo', - } - } - - assert await async_setup_component(hass, 'air_pollutants', config) - - state = hass.states.get('air_pollutants.demo_air_pollutants_home') - assert state is not None - - assert state.state == '14' - - -async def test_attributes(hass): - """Test Air Pollutants attributes.""" - config = { - 'air_pollutants': { - 'platform': 'demo', - } - } - - assert await async_setup_component(hass, 'air_pollutants', config) - - state = hass.states.get('air_pollutants.demo_air_pollutants_office') - assert state is not None - - data = state.attributes - assert data.get(ATTR_AIR_POLLUTANTS_PM_10) == 16 - assert data.get(ATTR_AIR_POLLUTANTS_N2O) is None - assert data.get(ATTR_AIR_POLLUTANTS_OZONE) is None - assert data.get(ATTR_AIR_POLLUTANTS_ATTRIBUTION) == \ - 'Powered by Home Assistant' diff --git a/tests/components/air_quality/__init__.py b/tests/components/air_quality/__init__.py new file mode 100644 index 00000000000..c53122cb1b9 --- /dev/null +++ b/tests/components/air_quality/__init__.py @@ -0,0 +1 @@ +"""The tests for Air Quality platforms.""" diff --git a/tests/components/air_quality/test_air_quality.py b/tests/components/air_quality/test_air_quality.py new file mode 100644 index 00000000000..7ad1300b945 --- /dev/null +++ b/tests/components/air_quality/test_air_quality.py @@ -0,0 +1,42 @@ +"""The tests for the Air Quality component.""" +from homeassistant.components.air_quality import ( + ATTR_ATTRIBUTION, ATTR_N2O, + ATTR_OZONE, ATTR_PM_10) +from homeassistant.setup import async_setup_component + + +async def test_state(hass): + """Test Air Quality state.""" + config = { + 'air_quality': { + 'platform': 'demo', + } + } + + assert await async_setup_component(hass, 'air_quality', config) + + state = hass.states.get('air_quality.demo_air_quality_home') + assert state is not None + + assert state.state == '14' + + +async def test_attributes(hass): + """Test Air Quality attributes.""" + config = { + 'air_quality': { + 'platform': 'demo', + } + } + + assert await async_setup_component(hass, 'air_quality', config) + + state = hass.states.get('air_quality.demo_air_quality_office') + assert state is not None + + data = state.attributes + assert data.get(ATTR_PM_10) == 16 + assert data.get(ATTR_N2O) is None + assert data.get(ATTR_OZONE) is None + assert data.get(ATTR_ATTRIBUTION) == \ + 'Powered by Home Assistant' From 09ff2722906cbe4198202186e11e080859282194 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Sat, 5 Jan 2019 22:31:41 +0100 Subject: [PATCH 013/235] Refactor motion sensor --- .../components/binary_sensor/xiaomi_aqara.py | 44 ++++++++++++------- 1 file changed, 29 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/binary_sensor/xiaomi_aqara.py b/homeassistant/components/binary_sensor/xiaomi_aqara.py index de259c718f4..1b0548ddfaf 100644 --- a/homeassistant/components/binary_sensor/xiaomi_aqara.py +++ b/homeassistant/components/binary_sensor/xiaomi_aqara.py @@ -178,7 +178,28 @@ class XiaomiMotionSensor(XiaomiBinarySensor): self.async_schedule_update_ha_state() def parse_data(self, data, raw_data): - """Parse data sent by gateway.""" + """Parse data sent by gateway. + + Polling (proto v1, firmware version 1.4.1_159.0143) + + >> { "cmd":"read","sid":"158..."} + << {'model': 'motion', 'sid': '158...', 'short_id': 26331, + 'cmd': 'read_ack', 'data': '{"voltage":3005}'} + + Multicast messages (proto v1, firmware version 1.4.1_159.0143) + + << {'model': 'motion', 'sid': '158...', 'short_id': 26331, + 'cmd': 'report', 'data': '{"status":"motion"}'} + << {'model': 'motion', 'sid': '158...', 'short_id': 26331, + 'cmd': 'report', 'data': '{"no_motion":"120"}'} + << {'model': 'motion', 'sid': '158...', 'short_id': 26331, + 'cmd': 'report', 'data': '{"no_motion":"180"}'} + << {'model': 'motion', 'sid': '158...', 'short_id': 26331, + 'cmd': 'report', 'data': '{"no_motion":"300"}'} + << {'model': 'motion', 'sid': '158...', 'short_id': 26331, + 'cmd': 'heartbeat', 'data': '{"voltage":3005}'} + + """ if raw_data['cmd'] == 'heartbeat': _LOGGER.debug( 'Skipping heartbeat of the motion sensor. ' @@ -187,8 +208,7 @@ class XiaomiMotionSensor(XiaomiBinarySensor): '11631#issuecomment-357507744).') return - self._should_poll = False - if NO_MOTION in data: # handle push from the hub + if NO_MOTION in data: self._no_motion_since = data[NO_MOTION] self._state = False return True @@ -203,26 +223,20 @@ class XiaomiMotionSensor(XiaomiBinarySensor): self._unsub_set_no_motion() self._unsub_set_no_motion = async_call_later( self._hass, - 180, + 120, self._async_set_no_motion ) - else: - self._should_poll = True - if self.entity_id is not None: - self._hass.bus.fire('xiaomi_aqara.motion', { - 'entity_id': self.entity_id - }) + + if self.entity_id is not None: + self._hass.bus.fire('xiaomi_aqara.motion', { + 'entity_id': self.entity_id + }) self._no_motion_since = 0 if self._state: return False self._state = True return True - if value == NO_MOTION: - if not self._state: - return False - self._state = False - return True class XiaomiDoorSensor(XiaomiBinarySensor): From 32faf5b70947b0c8c90f5bedf5498e9ae6dd23aa Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Sat, 5 Jan 2019 22:39:02 +0100 Subject: [PATCH 014/235] Improve debug output --- 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 1b0548ddfaf..afffb71bae5 100644 --- a/homeassistant/components/binary_sensor/xiaomi_aqara.py +++ b/homeassistant/components/binary_sensor/xiaomi_aqara.py @@ -107,7 +107,7 @@ class XiaomiBinarySensor(XiaomiDevice, BinarySensorDevice): def update(self): """Update the sensor state.""" - _LOGGER.debug('Updating xiaomi sensor by polling') + _LOGGER.debug('Updating xiaomi sensor (%s) by polling', self._sid) self._get_from_hub(self._sid) From 3d0c3ab746f92d626b877380a8310c409bc79208 Mon Sep 17 00:00:00 2001 From: Mattias Welponer Date: Sun, 6 Jan 2019 00:25:36 +0100 Subject: [PATCH 015/235] HomematicIP update version to 0.10.1 (#19788) * Update version to 0.10.1 * Update of requirements files --- homeassistant/components/homematicip_cloud/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homematicip_cloud/__init__.py b/homeassistant/components/homematicip_cloud/__init__.py index 05c5c970d2e..597d1660836 100644 --- a/homeassistant/components/homematicip_cloud/__init__.py +++ b/homeassistant/components/homematicip_cloud/__init__.py @@ -19,7 +19,7 @@ from .const import ( from .device import HomematicipGenericDevice # noqa: F401 from .hap import HomematicipAuth, HomematicipHAP # noqa: F401 -REQUIREMENTS = ['homematicip==0.9.8'] +REQUIREMENTS = ['homematicip==0.10.1'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 116713e247c..3f98fb7e2e1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -520,7 +520,7 @@ homeassistant-pyozw==0.1.1 # homekit==0.12.0 # homeassistant.components.homematicip_cloud -homematicip==0.9.8 +homematicip==0.10.1 # homeassistant.components.google # homeassistant.components.remember_the_milk diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 98e8cd54b7a..ecb147c7e81 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -107,7 +107,7 @@ holidays==0.9.8 home-assistant-frontend==20181219.0 # homeassistant.components.homematicip_cloud -homematicip==0.9.8 +homematicip==0.10.1 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From 76c30aca38d9c386ab4825439694646083a9bc59 Mon Sep 17 00:00:00 2001 From: emontnemery Date: Sun, 6 Jan 2019 06:27:57 +0100 Subject: [PATCH 016/235] Remove duplicated MQTT switch test case (#19799) --- tests/components/switch/test_mqtt.py | 49 ---------------------------- 1 file changed, 49 deletions(-) diff --git a/tests/components/switch/test_mqtt.py b/tests/components/switch/test_mqtt.py index cbacc9d5335..c1aa7c3a212 100644 --- a/tests/components/switch/test_mqtt.py +++ b/tests/components/switch/test_mqtt.py @@ -128,55 +128,6 @@ async def test_controlling_state_via_topic_and_json_message( assert STATE_OFF == state.state -async def test_controlling_availability(hass, mock_publish): - """Test the controlling state via topic.""" - assert await async_setup_component(hass, switch.DOMAIN, { - switch.DOMAIN: { - 'platform': 'mqtt', - 'name': 'test', - 'state_topic': 'state-topic', - 'command_topic': 'command-topic', - 'availability_topic': 'availability_topic', - 'payload_on': 1, - 'payload_off': 0, - 'payload_available': 1, - 'payload_not_available': 0 - } - }) - - state = hass.states.get('switch.test') - assert STATE_UNAVAILABLE == state.state - - async_fire_mqtt_message(hass, 'availability_topic', '1') - await hass.async_block_till_done() - await hass.async_block_till_done() - - state = hass.states.get('switch.test') - assert STATE_OFF == state.state - assert not state.attributes.get(ATTR_ASSUMED_STATE) - - async_fire_mqtt_message(hass, 'availability_topic', '0') - await hass.async_block_till_done() - await hass.async_block_till_done() - - state = hass.states.get('switch.test') - assert STATE_UNAVAILABLE == state.state - - async_fire_mqtt_message(hass, 'state-topic', '1') - await hass.async_block_till_done() - await hass.async_block_till_done() - - state = hass.states.get('switch.test') - assert STATE_UNAVAILABLE == state.state - - async_fire_mqtt_message(hass, 'availability_topic', '1') - await hass.async_block_till_done() - await hass.async_block_till_done() - - state = hass.states.get('switch.test') - assert STATE_ON == state.state - - async def test_default_availability_payload(hass, mock_publish): """Test the availability payload.""" assert await async_setup_component(hass, switch.DOMAIN, { From dee229152fec9c1992d7718d0dfa43896dd4821c Mon Sep 17 00:00:00 2001 From: Tommy Jonsson Date: Sun, 6 Jan 2019 14:16:46 +0100 Subject: [PATCH 017/235] [1/3] Refactor mqtt-vacuum in preparation for discovery and device registry (#19462) * Refactor mqtt-vacuum in preparation for discovery and device registry --- homeassistant/components/vacuum/mqtt.py | 272 ++++++++++-------------- 1 file changed, 117 insertions(+), 155 deletions(-) diff --git a/homeassistant/components/vacuum/mqtt.py b/homeassistant/components/vacuum/mqtt.py index a017745a715..ac15f4ce048 100644 --- a/homeassistant/components/vacuum/mqtt.py +++ b/homeassistant/components/vacuum/mqtt.py @@ -9,7 +9,8 @@ import logging import voluptuous as vol from homeassistant.components import mqtt -from homeassistant.components.mqtt import MqttAvailability +from homeassistant.components.mqtt import ( + MqttAvailability, subscription) from homeassistant.components.vacuum import ( SUPPORT_BATTERY, SUPPORT_CLEAN_SPOT, SUPPORT_FAN_SPEED, SUPPORT_LOCATE, SUPPORT_PAUSE, SUPPORT_RETURN_HOME, SUPPORT_SEND_COMMAND, @@ -145,131 +146,14 @@ PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({ async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the vacuum.""" - name = config.get(CONF_NAME) - supported_feature_strings = config.get(CONF_SUPPORTED_FEATURES) - supported_features = strings_to_services(supported_feature_strings) - - qos = config.get(mqtt.CONF_QOS) - retain = config.get(mqtt.CONF_RETAIN) - - command_topic = config.get(mqtt.CONF_COMMAND_TOPIC) - payload_turn_on = config.get(CONF_PAYLOAD_TURN_ON) - payload_turn_off = config.get(CONF_PAYLOAD_TURN_OFF) - payload_return_to_base = config.get(CONF_PAYLOAD_RETURN_TO_BASE) - payload_stop = config.get(CONF_PAYLOAD_STOP) - payload_clean_spot = config.get(CONF_PAYLOAD_CLEAN_SPOT) - payload_locate = config.get(CONF_PAYLOAD_LOCATE) - payload_start_pause = config.get(CONF_PAYLOAD_START_PAUSE) - - battery_level_topic = config.get(CONF_BATTERY_LEVEL_TOPIC) - battery_level_template = config.get(CONF_BATTERY_LEVEL_TEMPLATE) - if battery_level_template: - battery_level_template.hass = hass - - charging_topic = config.get(CONF_CHARGING_TOPIC) - charging_template = config.get(CONF_CHARGING_TEMPLATE) - if charging_template: - charging_template.hass = hass - - cleaning_topic = config.get(CONF_CLEANING_TOPIC) - cleaning_template = config.get(CONF_CLEANING_TEMPLATE) - if cleaning_template: - cleaning_template.hass = hass - - docked_topic = config.get(CONF_DOCKED_TOPIC) - docked_template = config.get(CONF_DOCKED_TEMPLATE) - if docked_template: - docked_template.hass = hass - - error_topic = config.get(CONF_ERROR_TOPIC) - error_template = config.get(CONF_ERROR_TEMPLATE) - if error_template: - error_template.hass = hass - - fan_speed_topic = config.get(CONF_FAN_SPEED_TOPIC) - fan_speed_template = config.get(CONF_FAN_SPEED_TEMPLATE) - if fan_speed_template: - fan_speed_template.hass = hass - - set_fan_speed_topic = config.get(CONF_SET_FAN_SPEED_TOPIC) - fan_speed_list = config.get(CONF_FAN_SPEED_LIST) - - send_command_topic = config.get(CONF_SEND_COMMAND_TOPIC) - - availability_topic = config.get(mqtt.CONF_AVAILABILITY_TOPIC) - payload_available = config.get(mqtt.CONF_PAYLOAD_AVAILABLE) - payload_not_available = config.get(mqtt.CONF_PAYLOAD_NOT_AVAILABLE) - - async_add_entities([ - MqttVacuum( - name, supported_features, qos, retain, command_topic, - payload_turn_on, payload_turn_off, payload_return_to_base, - payload_stop, payload_clean_spot, payload_locate, - payload_start_pause, battery_level_topic, battery_level_template, - charging_topic, charging_template, cleaning_topic, - cleaning_template, docked_topic, docked_template, - error_topic, error_template, fan_speed_topic, - fan_speed_template, set_fan_speed_topic, fan_speed_list, - send_command_topic, availability_topic, payload_available, - payload_not_available - ), - ]) + async_add_entities([MqttVacuum(config, discovery_info)]) class MqttVacuum(MqttAvailability, VacuumDevice): """Representation of a MQTT-controlled vacuum.""" - def __init__( - self, name, supported_features, qos, retain, command_topic, - payload_turn_on, payload_turn_off, payload_return_to_base, - payload_stop, payload_clean_spot, payload_locate, - payload_start_pause, battery_level_topic, battery_level_template, - charging_topic, charging_template, cleaning_topic, - cleaning_template, docked_topic, docked_template, - error_topic, error_template, fan_speed_topic, - fan_speed_template, set_fan_speed_topic, fan_speed_list, - send_command_topic, availability_topic, payload_available, - payload_not_available): + def __init__(self, config, discovery_info): """Initialize the vacuum.""" - super().__init__(availability_topic, qos, payload_available, - payload_not_available) - - self._name = name - self._supported_features = supported_features - self._qos = qos - self._retain = retain - - self._command_topic = command_topic - self._payload_turn_on = payload_turn_on - self._payload_turn_off = payload_turn_off - self._payload_return_to_base = payload_return_to_base - self._payload_stop = payload_stop - self._payload_clean_spot = payload_clean_spot - self._payload_locate = payload_locate - self._payload_start_pause = payload_start_pause - - self._battery_level_topic = battery_level_topic - self._battery_level_template = battery_level_template - - self._charging_topic = charging_topic - self._charging_template = charging_template - - self._cleaning_topic = cleaning_topic - self._cleaning_template = cleaning_template - - self._docked_topic = docked_topic - self._docked_template = docked_template - - self._error_topic = error_topic - self._error_template = error_template - - self._fan_speed_topic = fan_speed_topic - self._fan_speed_template = fan_speed_template - - self._set_fan_speed_topic = set_fan_speed_topic - self._fan_speed_list = fan_speed_list - self._send_command_topic = send_command_topic - self._cleaning = False self._charging = False self._docked = False @@ -277,45 +161,115 @@ class MqttVacuum(MqttAvailability, VacuumDevice): self._status = 'Unknown' self._battery_level = 0 self._fan_speed = 'unknown' + self._fan_speed_list = [] + self._sub_state = None + + # Load config + self._setup_from_config(config) + + qos = config.get(mqtt.CONF_QOS) + availability_topic = config.get(mqtt.CONF_AVAILABILITY_TOPIC) + payload_available = config.get(mqtt.CONF_PAYLOAD_AVAILABLE) + payload_not_available = config.get(mqtt.CONF_PAYLOAD_NOT_AVAILABLE) + + MqttAvailability.__init__(self, availability_topic, qos, + payload_available, payload_not_available) + + def _setup_from_config(self, config): + self._name = config.get(CONF_NAME) + supported_feature_strings = config.get(CONF_SUPPORTED_FEATURES) + self._supported_features = strings_to_services( + supported_feature_strings + ) + self._fan_speed_list = config.get(CONF_FAN_SPEED_LIST) + self._qos = config.get(mqtt.CONF_QOS) + self._retain = config.get(mqtt.CONF_RETAIN) + + self._command_topic = config.get(mqtt.CONF_COMMAND_TOPIC) + self._set_fan_speed_topic = config.get(CONF_SET_FAN_SPEED_TOPIC) + self._send_command_topic = config.get(CONF_SEND_COMMAND_TOPIC) + + self._payloads = { + key: config.get(key) for key in ( + CONF_PAYLOAD_TURN_ON, + CONF_PAYLOAD_TURN_OFF, + CONF_PAYLOAD_RETURN_TO_BASE, + CONF_PAYLOAD_STOP, + CONF_PAYLOAD_CLEAN_SPOT, + CONF_PAYLOAD_LOCATE, + CONF_PAYLOAD_START_PAUSE + ) + } + self._state_topics = { + key: config.get(key) for key in ( + CONF_BATTERY_LEVEL_TOPIC, + CONF_CHARGING_TOPIC, + CONF_CLEANING_TOPIC, + CONF_DOCKED_TOPIC, + CONF_ERROR_TOPIC, + CONF_FAN_SPEED_TOPIC + ) + } + self._templates = { + key: config.get(key) for key in ( + CONF_BATTERY_LEVEL_TEMPLATE, + CONF_CHARGING_TEMPLATE, + CONF_CLEANING_TEMPLATE, + CONF_DOCKED_TEMPLATE, + CONF_ERROR_TEMPLATE, + CONF_FAN_SPEED_TEMPLATE + ) + } async def async_added_to_hass(self): """Subscribe MQTT events.""" await super().async_added_to_hass() + await self._subscribe_topics() + + async def _subscribe_topics(self): + """(Re)Subscribe to topics.""" + for tpl in self._templates.values(): + if tpl is not None: + tpl.hass = self.hass @callback def message_received(topic, payload, qos): """Handle new MQTT message.""" - if topic == self._battery_level_topic and \ - self._battery_level_template: - battery_level = self._battery_level_template\ + if topic == self._state_topics[CONF_BATTERY_LEVEL_TOPIC] and \ + self._templates[CONF_BATTERY_LEVEL_TEMPLATE]: + battery_level = self._templates[CONF_BATTERY_LEVEL_TEMPLATE]\ .async_render_with_possible_json_value( payload, error_value=None) if battery_level is not None: self._battery_level = int(battery_level) - if topic == self._charging_topic and self._charging_template: - charging = self._charging_template\ + if topic == self._state_topics[CONF_CHARGING_TOPIC] and \ + self._templates[CONF_CHARGING_TEMPLATE]: + charging = self._templates[CONF_CHARGING_TEMPLATE]\ .async_render_with_possible_json_value( payload, error_value=None) if charging is not None: self._charging = cv.boolean(charging) - if topic == self._cleaning_topic and self._cleaning_template: - cleaning = self._cleaning_template \ + if topic == self._state_topics[CONF_CLEANING_TOPIC] and \ + self._templates[CONF_CLEANING_TEMPLATE]: + cleaning = self._templates[CONF_CLEANING_TEMPLATE]\ .async_render_with_possible_json_value( payload, error_value=None) if cleaning is not None: self._cleaning = cv.boolean(cleaning) - if topic == self._docked_topic and self._docked_template: - docked = self._docked_template \ + if topic == self._state_topics[CONF_DOCKED_TOPIC] and \ + self._templates[CONF_DOCKED_TEMPLATE]: + docked = self._templates[CONF_DOCKED_TEMPLATE]\ .async_render_with_possible_json_value( payload, error_value=None) if docked is not None: self._docked = cv.boolean(docked) - if topic == self._error_topic and self._error_template: - error = self._error_template \ + if topic == self._state_topics[CONF_ERROR_TOPIC] and \ + self._templates[CONF_ERROR_TEMPLATE]: + error = self._templates[CONF_ERROR_TEMPLATE]\ .async_render_with_possible_json_value( payload, error_value=None) if error is not None: @@ -333,8 +287,9 @@ class MqttVacuum(MqttAvailability, VacuumDevice): else: self._status = "Stopped" - if topic == self._fan_speed_topic and self._fan_speed_template: - fan_speed = self._fan_speed_template\ + if topic == self._state_topics[CONF_FAN_SPEED_TOPIC] and \ + self._templates[CONF_FAN_SPEED_TEMPLATE]: + fan_speed = self._templates[CONF_FAN_SPEED_TEMPLATE]\ .async_render_with_possible_json_value( payload, error_value=None) if fan_speed is not None: @@ -342,14 +297,17 @@ class MqttVacuum(MqttAvailability, VacuumDevice): self.async_schedule_update_ha_state() - topics_list = [topic for topic in (self._battery_level_topic, - self._charging_topic, - self._cleaning_topic, - self._docked_topic, - self._fan_speed_topic) if topic] - for topic in set(topics_list): - await self.hass.components.mqtt.async_subscribe( - topic, message_received, self._qos) + topics_list = {topic for topic in self._state_topics.values() if topic} + self._sub_state = await subscription.async_subscribe_topics( + self.hass, self._sub_state, + { + "topic{}".format(i): { + "topic": topic, + "msg_callback": message_received, + "qos": self._qos + } for i, topic in enumerate(topics_list) + } + ) @property def name(self): @@ -417,7 +375,8 @@ class MqttVacuum(MqttAvailability, VacuumDevice): return mqtt.async_publish(self.hass, self._command_topic, - self._payload_turn_on, self._qos, self._retain) + self._payloads[CONF_PAYLOAD_TURN_ON], + self._qos, self._retain) self._status = 'Cleaning' self.async_schedule_update_ha_state() @@ -427,7 +386,8 @@ class MqttVacuum(MqttAvailability, VacuumDevice): return mqtt.async_publish(self.hass, self._command_topic, - self._payload_turn_off, self._qos, self._retain) + self._payloads[CONF_PAYLOAD_TURN_OFF], + self._qos, self._retain) self._status = 'Turning Off' self.async_schedule_update_ha_state() @@ -436,7 +396,8 @@ class MqttVacuum(MqttAvailability, VacuumDevice): if self.supported_features & SUPPORT_STOP == 0: return - mqtt.async_publish(self.hass, self._command_topic, self._payload_stop, + mqtt.async_publish(self.hass, self._command_topic, + self._payloads[CONF_PAYLOAD_STOP], self._qos, self._retain) self._status = 'Stopping the current task' self.async_schedule_update_ha_state() @@ -447,7 +408,8 @@ class MqttVacuum(MqttAvailability, VacuumDevice): return mqtt.async_publish(self.hass, self._command_topic, - self._payload_clean_spot, self._qos, self._retain) + self._payloads[CONF_PAYLOAD_CLEAN_SPOT], + self._qos, self._retain) self._status = "Cleaning spot" self.async_schedule_update_ha_state() @@ -457,7 +419,8 @@ class MqttVacuum(MqttAvailability, VacuumDevice): return mqtt.async_publish(self.hass, self._command_topic, - self._payload_locate, self._qos, self._retain) + self._payloads[CONF_PAYLOAD_LOCATE], + self._qos, self._retain) self._status = "Hi, I'm over here!" self.async_schedule_update_ha_state() @@ -467,7 +430,8 @@ class MqttVacuum(MqttAvailability, VacuumDevice): return mqtt.async_publish(self.hass, self._command_topic, - self._payload_start_pause, self._qos, self._retain) + self._payloads[CONF_PAYLOAD_START_PAUSE], + self._qos, self._retain) self._status = 'Pausing/Resuming cleaning...' self.async_schedule_update_ha_state() @@ -477,8 +441,8 @@ class MqttVacuum(MqttAvailability, VacuumDevice): return mqtt.async_publish(self.hass, self._command_topic, - self._payload_return_to_base, self._qos, - self._retain) + self._payloads[CONF_PAYLOAD_RETURN_TO_BASE], + self._qos, self._retain) self._status = 'Returning home...' self.async_schedule_update_ha_state() @@ -489,9 +453,8 @@ class MqttVacuum(MqttAvailability, VacuumDevice): if not self._fan_speed_list or fan_speed not in self._fan_speed_list: return - mqtt.async_publish( - self.hass, self._set_fan_speed_topic, fan_speed, self._qos, - self._retain) + mqtt.async_publish(self.hass, self._set_fan_speed_topic, + fan_speed, self._qos, self._retain) self._status = "Setting fan to {}...".format(fan_speed) self.async_schedule_update_ha_state() @@ -500,8 +463,7 @@ class MqttVacuum(MqttAvailability, VacuumDevice): if self.supported_features & SUPPORT_SEND_COMMAND == 0: return - mqtt.async_publish( - self.hass, self._send_command_topic, command, self._qos, - self._retain) + mqtt.async_publish(self.hass, self._send_command_topic, + command, self._qos, self._retain) self._status = "Sending command {}...".format(command) self.async_schedule_update_ha_state() From ccbc231d3ae42adf05e1fa1612e96142b18dd090 Mon Sep 17 00:00:00 2001 From: Tommy Jonsson Date: Sun, 6 Jan 2019 17:05:04 +0100 Subject: [PATCH 018/235] [2/3] vacuum mqtt-discovery (#19478) * add discoverability to mqtt-vacuum --- homeassistant/components/mqtt/discovery.py | 18 +- homeassistant/components/vacuum/__init__.py | 12 +- homeassistant/components/vacuum/mqtt.py | 47 +- tests/components/vacuum/common.py | 121 ++++- tests/components/vacuum/test_mqtt.py | 492 ++++++++++++-------- 5 files changed, 464 insertions(+), 226 deletions(-) diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py index 92e7ffdaf4f..790007dc0e8 100644 --- a/homeassistant/components/mqtt/discovery.py +++ b/homeassistant/components/mqtt/discovery.py @@ -25,7 +25,7 @@ TOPIC_MATCHER = re.compile( SUPPORTED_COMPONENTS = [ 'binary_sensor', 'camera', 'cover', 'fan', 'light', 'sensor', 'switch', 'lock', 'climate', - 'alarm_control_panel'] + 'alarm_control_panel', 'vacuum'] CONFIG_ENTRY_COMPONENTS = [ 'binary_sensor', @@ -38,6 +38,7 @@ CONFIG_ENTRY_COMPONENTS = [ 'climate', 'alarm_control_panel', 'fan', + 'vacuum', ] DEPRECATED_PLATFORM_TO_SCHEMA = { @@ -69,12 +70,25 @@ ABBREVIATIONS = { 'bri_stat_t': 'brightness_state_topic', 'bri_val_tpl': 'brightness_value_template', 'clr_temp_cmd_tpl': 'color_temp_command_template', + 'bat_lev_t': 'battery_level_topic', + 'bat_lev_tpl': 'battery_level_template', + 'chrg_t': 'charging_topic', + 'chrg_tpl': 'charging_template', 'clr_temp_cmd_t': 'color_temp_command_topic', 'clr_temp_stat_t': 'color_temp_state_topic', 'clr_temp_val_tpl': 'color_temp_value_template', + 'cln_t': 'cleaning_topic', + 'cln_tpl': 'cleaning_template', 'cmd_t': 'command_topic', 'curr_temp_t': 'current_temperature_topic', 'dev_cla': 'device_class', + 'dock_t': 'docked_topic', + 'dock_tpl': 'docked_template', + 'err_t': 'error_topic', + 'err_tpl': 'error_template', + 'fanspd_t': 'fan_speed_topic', + 'fanspd_tpl': 'fan_speed_template', + 'fanspd_lst': 'fan_speed_list', 'fx_cmd_t': 'effect_command_topic', 'fx_list': 'effect_list', 'fx_stat_t': 'effect_state_topic', @@ -124,6 +138,7 @@ ABBREVIATIONS = { 'rgb_cmd_t': 'rgb_command_topic', 'rgb_stat_t': 'rgb_state_topic', 'rgb_val_tpl': 'rgb_value_template', + 'send_cmd_t': 'send_command_topic', 'send_if_off': 'send_if_off', 'set_pos_tpl': 'set_position_template', 'set_pos_t': 'set_position_topic', @@ -137,6 +152,7 @@ ABBREVIATIONS = { 'stat_open': 'state_open', 'stat_t': 'state_topic', 'stat_val_tpl': 'state_value_template', + 'sup_feat': 'supported_features', 'swing_mode_cmd_t': 'swing_mode_command_topic', 'swing_mode_stat_tpl': 'swing_mode_state_template', 'swing_mode_stat_t': 'swing_mode_state_topic', diff --git a/homeassistant/components/vacuum/__init__.py b/homeassistant/components/vacuum/__init__.py index 4799e945be0..6341c9661ed 100644 --- a/homeassistant/components/vacuum/__init__.py +++ b/homeassistant/components/vacuum/__init__.py @@ -95,7 +95,7 @@ def is_on(hass, entity_id=None): async def async_setup(hass, config): """Set up the vacuum component.""" - component = EntityComponent( + component = hass.data[DOMAIN] = EntityComponent( _LOGGER, DOMAIN, hass, SCAN_INTERVAL, GROUP_NAME_ALL_VACUUMS) await component.async_setup(config) @@ -152,6 +152,16 @@ async def async_setup(hass, config): return True +async def async_setup_entry(hass, entry): + """Set up a config entry.""" + return await hass.data[DOMAIN].async_setup_entry(entry) + + +async def async_unload_entry(hass, entry): + """Unload a config entry.""" + return await hass.data[DOMAIN].async_unload_entry(entry) + + class _BaseVacuum(Entity): """Representation of a base vacuum. diff --git a/homeassistant/components/vacuum/mqtt.py b/homeassistant/components/vacuum/mqtt.py index ac15f4ce048..eeed7090ebf 100644 --- a/homeassistant/components/vacuum/mqtt.py +++ b/homeassistant/components/vacuum/mqtt.py @@ -10,15 +10,18 @@ import voluptuous as vol from homeassistant.components import mqtt from homeassistant.components.mqtt import ( - MqttAvailability, subscription) + ATTR_DISCOVERY_HASH, MqttAvailability, MqttDiscoveryUpdate, + subscription) +from homeassistant.components.mqtt.discovery import MQTT_DISCOVERY_NEW from homeassistant.components.vacuum import ( SUPPORT_BATTERY, SUPPORT_CLEAN_SPOT, SUPPORT_FAN_SPEED, SUPPORT_LOCATE, SUPPORT_PAUSE, SUPPORT_RETURN_HOME, SUPPORT_SEND_COMMAND, SUPPORT_STATUS, SUPPORT_STOP, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, - VacuumDevice) + VacuumDevice, DOMAIN) from homeassistant.const import ATTR_SUPPORTED_FEATURES, CONF_NAME from homeassistant.core import callback import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.icon import icon_for_battery_level _LOGGER = logging.getLogger(__name__) @@ -145,11 +148,30 @@ PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({ async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the vacuum.""" - async_add_entities([MqttVacuum(config, discovery_info)]) + """Set up MQTT vacuum through configuration.yaml.""" + await _async_setup_entity(config, async_add_entities, + discovery_info) -class MqttVacuum(MqttAvailability, VacuumDevice): +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up MQTT vacuum dynamically through MQTT discovery.""" + async def async_discover(discovery_payload): + """Discover and add a MQTT vacuum.""" + config = PLATFORM_SCHEMA(discovery_payload) + await _async_setup_entity(config, async_add_entities, + discovery_payload[ATTR_DISCOVERY_HASH]) + + async_dispatcher_connect( + hass, MQTT_DISCOVERY_NEW.format(DOMAIN, 'mqtt'), async_discover) + + +async def _async_setup_entity(config, async_add_entities, + discovery_hash=None): + """Set up the MQTT vacuum.""" + async_add_entities([MqttVacuum(config, discovery_hash)]) + + +class MqttVacuum(MqttAvailability, MqttDiscoveryUpdate, VacuumDevice): """Representation of a MQTT-controlled vacuum.""" def __init__(self, config, discovery_info): @@ -174,6 +196,8 @@ class MqttVacuum(MqttAvailability, VacuumDevice): MqttAvailability.__init__(self, availability_topic, qos, payload_available, payload_not_available) + MqttDiscoveryUpdate.__init__(self, discovery_info, + self.discovery_update) def _setup_from_config(self, config): self._name = config.get(CONF_NAME) @@ -221,11 +245,24 @@ class MqttVacuum(MqttAvailability, VacuumDevice): ) } + async def discovery_update(self, discovery_payload): + """Handle updated discovery message.""" + config = PLATFORM_SCHEMA(discovery_payload) + self._setup_from_config(config) + await self.availability_discovery_update(config) + await self._subscribe_topics() + self.async_schedule_update_ha_state() + async def async_added_to_hass(self): """Subscribe MQTT events.""" await super().async_added_to_hass() await self._subscribe_topics() + async def async_will_remove_from_hass(self): + """Unsubscribe when removed.""" + await subscription.async_unsubscribe_topics(self.hass, self._sub_state) + await MqttAvailability.async_will_remove_from_hass(self) + async def _subscribe_topics(self): """(Re)Subscribe to topics.""" for tpl in self._templates.values(): diff --git a/tests/components/vacuum/common.py b/tests/components/vacuum/common.py index 436f23f5546..62a0e429c0a 100644 --- a/tests/components/vacuum/common.py +++ b/tests/components/vacuum/common.py @@ -10,92 +10,189 @@ from homeassistant.components.vacuum import ( from homeassistant.const import ( ATTR_COMMAND, ATTR_ENTITY_ID, SERVICE_TOGGLE, SERVICE_TURN_OFF, SERVICE_TURN_ON) +from homeassistant.core import callback from homeassistant.loader import bind_hass @bind_hass def turn_on(hass, entity_id=None): + """Turn all or specified vacuum on.""" + hass.add_job(async_turn_on, hass, entity_id) + + +@callback +@bind_hass +def async_turn_on(hass, entity_id=None): """Turn all or specified vacuum on.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else None - hass.services.call(DOMAIN, SERVICE_TURN_ON, data) + hass.async_add_job(hass.services.async_call( + DOMAIN, SERVICE_TURN_ON, data)) @bind_hass def turn_off(hass, entity_id=None): + """Turn all or specified vacuum off.""" + hass.add_job(async_turn_off, hass, entity_id) + + +@callback +@bind_hass +def async_turn_off(hass, entity_id=None): """Turn all or specified vacuum off.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else None - hass.services.call(DOMAIN, SERVICE_TURN_OFF, data) + hass.async_add_job(hass.services.async_call( + DOMAIN, SERVICE_TURN_OFF, data)) @bind_hass def toggle(hass, entity_id=None): + """Toggle all or specified vacuum.""" + hass.add_job(async_toggle, hass, entity_id) + + +@callback +@bind_hass +def async_toggle(hass, entity_id=None): """Toggle all or specified vacuum.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else None - hass.services.call(DOMAIN, SERVICE_TOGGLE, data) + hass.async_add_job(hass.services.async_call( + DOMAIN, SERVICE_TOGGLE, data)) @bind_hass def locate(hass, entity_id=None): + """Locate all or specified vacuum.""" + hass.add_job(async_locate, hass, entity_id) + + +@callback +@bind_hass +def async_locate(hass, entity_id=None): """Locate all or specified vacuum.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else None - hass.services.call(DOMAIN, SERVICE_LOCATE, data) + hass.async_add_job(hass.services.async_call( + DOMAIN, SERVICE_LOCATE, data)) @bind_hass def clean_spot(hass, entity_id=None): + """Tell all or specified vacuum to perform a spot clean-up.""" + hass.add_job(async_clean_spot, hass, entity_id) + + +@callback +@bind_hass +def async_clean_spot(hass, entity_id=None): """Tell all or specified vacuum to perform a spot clean-up.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else None - hass.services.call(DOMAIN, SERVICE_CLEAN_SPOT, data) + hass.async_add_job(hass.services.async_call( + DOMAIN, SERVICE_CLEAN_SPOT, data)) @bind_hass def return_to_base(hass, entity_id=None): + """Tell all or specified vacuum to return to base.""" + hass.add_job(async_return_to_base, hass, entity_id) + + +@callback +@bind_hass +def async_return_to_base(hass, entity_id=None): """Tell all or specified vacuum to return to base.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else None - hass.services.call(DOMAIN, SERVICE_RETURN_TO_BASE, data) + hass.async_add_job(hass.services.async_call( + DOMAIN, SERVICE_RETURN_TO_BASE, data)) @bind_hass def start_pause(hass, entity_id=None): + """Tell all or specified vacuum to start or pause the current task.""" + hass.add_job(async_start_pause, hass, entity_id) + + +@callback +@bind_hass +def async_start_pause(hass, entity_id=None): """Tell all or specified vacuum to start or pause the current task.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else None - hass.services.call(DOMAIN, SERVICE_START_PAUSE, data) + hass.async_add_job(hass.services.async_call( + DOMAIN, SERVICE_START_PAUSE, data)) @bind_hass def start(hass, entity_id=None): + """Tell all or specified vacuum to start or resume the current task.""" + hass.add_job(async_start, hass, entity_id) + + +@callback +@bind_hass +def async_start(hass, entity_id=None): """Tell all or specified vacuum to start or resume the current task.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else None - hass.services.call(DOMAIN, SERVICE_START, data) + hass.async_add_job(hass.services.async_call( + DOMAIN, SERVICE_START, data)) @bind_hass def pause(hass, entity_id=None): + """Tell all or the specified vacuum to pause the current task.""" + hass.add_job(async_pause, hass, entity_id) + + +@callback +@bind_hass +def async_pause(hass, entity_id=None): """Tell all or the specified vacuum to pause the current task.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else None - hass.services.call(DOMAIN, SERVICE_PAUSE, data) + hass.async_add_job(hass.services.async_call( + DOMAIN, SERVICE_PAUSE, data)) @bind_hass def stop(hass, entity_id=None): + """Stop all or specified vacuum.""" + hass.add_job(async_stop, hass, entity_id) + + +@callback +@bind_hass +def async_stop(hass, entity_id=None): """Stop all or specified vacuum.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else None - hass.services.call(DOMAIN, SERVICE_STOP, data) + hass.async_add_job(hass.services.async_call( + DOMAIN, SERVICE_STOP, data)) @bind_hass def set_fan_speed(hass, fan_speed, entity_id=None): + """Set fan speed for all or specified vacuum.""" + hass.add_job(async_set_fan_speed, hass, fan_speed, entity_id) + + +@callback +@bind_hass +def async_set_fan_speed(hass, fan_speed, entity_id=None): """Set fan speed for all or specified vacuum.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} data[ATTR_FAN_SPEED] = fan_speed - hass.services.call(DOMAIN, SERVICE_SET_FAN_SPEED, data) + hass.async_add_job(hass.services.async_call( + DOMAIN, SERVICE_SET_FAN_SPEED, data)) @bind_hass def send_command(hass, command, params=None, entity_id=None): + """Send command to all or specified vacuum.""" + hass.add_job(async_send_command, hass, command, params, entity_id) + + +@callback +@bind_hass +def async_send_command(hass, command, params=None, entity_id=None): """Send command to all or specified vacuum.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} data[ATTR_COMMAND] = command if params is not None: data[ATTR_PARAMS] = params - hass.services.call(DOMAIN, SERVICE_SEND_COMMAND, data) + hass.async_add_job(hass.services.async_call( + DOMAIN, SERVICE_SEND_COMMAND, data)) diff --git a/tests/components/vacuum/test_mqtt.py b/tests/components/vacuum/test_mqtt.py index ba2c1866807..3df6235f85c 100644 --- a/tests/components/vacuum/test_mqtt.py +++ b/tests/components/vacuum/test_mqtt.py @@ -1,254 +1,332 @@ -"""The tests for the Demo vacuum platform.""" -import unittest +"""The tests for the Mqtt vacuum platform.""" +import pytest -from homeassistant.components import vacuum -from homeassistant.components.vacuum import ( - ATTR_BATTERY_LEVEL, ATTR_BATTERY_ICON, ATTR_STATUS, - ATTR_FAN_SPEED, mqtt) -from homeassistant.components.mqtt import CONF_COMMAND_TOPIC +from homeassistant.setup import async_setup_component from homeassistant.const import ( CONF_PLATFORM, STATE_OFF, STATE_ON, STATE_UNAVAILABLE, CONF_NAME) -from homeassistant.setup import setup_component - +from homeassistant.components import vacuum, mqtt +from homeassistant.components.vacuum import ( + ATTR_BATTERY_LEVEL, ATTR_BATTERY_ICON, ATTR_STATUS, + ATTR_FAN_SPEED, mqtt as mqttvacuum) +from homeassistant.components.mqtt import CONF_COMMAND_TOPIC +from homeassistant.components.mqtt.discovery import async_start from tests.common import ( - fire_mqtt_message, get_test_home_assistant, mock_mqtt_component) + async_mock_mqtt_component, + async_fire_mqtt_message, MockConfigEntry) from tests.components.vacuum import common -class TestVacuumMQTT(unittest.TestCase): - """MQTT vacuum component test class.""" +default_config = { + CONF_PLATFORM: 'mqtt', + CONF_NAME: 'mqtttest', + CONF_COMMAND_TOPIC: 'vacuum/command', + mqttvacuum.CONF_SEND_COMMAND_TOPIC: 'vacuum/send_command', + mqttvacuum.CONF_BATTERY_LEVEL_TOPIC: 'vacuum/state', + mqttvacuum.CONF_BATTERY_LEVEL_TEMPLATE: + '{{ value_json.battery_level }}', + mqttvacuum.CONF_CHARGING_TOPIC: 'vacuum/state', + mqttvacuum.CONF_CHARGING_TEMPLATE: '{{ value_json.charging }}', + mqttvacuum.CONF_CLEANING_TOPIC: 'vacuum/state', + mqttvacuum.CONF_CLEANING_TEMPLATE: '{{ value_json.cleaning }}', + mqttvacuum.CONF_DOCKED_TOPIC: 'vacuum/state', + mqttvacuum.CONF_DOCKED_TEMPLATE: '{{ value_json.docked }}', + mqttvacuum.CONF_STATE_TOPIC: 'vacuum/state', + mqttvacuum.CONF_STATE_TEMPLATE: '{{ value_json.state }}', + mqttvacuum.CONF_FAN_SPEED_TOPIC: 'vacuum/state', + mqttvacuum.CONF_FAN_SPEED_TEMPLATE: '{{ value_json.fan_speed }}', + mqttvacuum.CONF_SET_FAN_SPEED_TOPIC: 'vacuum/set_fan_speed', + mqttvacuum.CONF_FAN_SPEED_LIST: ['min', 'medium', 'high', 'max'], +} - def setUp(self): # pylint: disable=invalid-name - """Set up things to be run when tests are started.""" - self.hass = get_test_home_assistant() - self.mock_publish = mock_mqtt_component(self.hass) - self.default_config = { - CONF_PLATFORM: 'mqtt', - CONF_NAME: 'mqtttest', - CONF_COMMAND_TOPIC: 'vacuum/command', - mqtt.CONF_SEND_COMMAND_TOPIC: 'vacuum/send_command', - mqtt.CONF_BATTERY_LEVEL_TOPIC: 'vacuum/state', - mqtt.CONF_BATTERY_LEVEL_TEMPLATE: - '{{ value_json.battery_level }}', - mqtt.CONF_CHARGING_TOPIC: 'vacuum/state', - mqtt.CONF_CHARGING_TEMPLATE: '{{ value_json.charging }}', - mqtt.CONF_CLEANING_TOPIC: 'vacuum/state', - mqtt.CONF_CLEANING_TEMPLATE: '{{ value_json.cleaning }}', - mqtt.CONF_DOCKED_TOPIC: 'vacuum/state', - mqtt.CONF_DOCKED_TEMPLATE: '{{ value_json.docked }}', - mqtt.CONF_STATE_TOPIC: 'vacuum/state', - mqtt.CONF_STATE_TEMPLATE: '{{ value_json.state }}', - mqtt.CONF_FAN_SPEED_TOPIC: 'vacuum/state', - mqtt.CONF_FAN_SPEED_TEMPLATE: '{{ value_json.fan_speed }}', - mqtt.CONF_SET_FAN_SPEED_TOPIC: 'vacuum/set_fan_speed', - mqtt.CONF_FAN_SPEED_LIST: ['min', 'medium', 'high', 'max'], - } +@pytest.fixture +def mock_publish(hass): + """Initialize components.""" + yield hass.loop.run_until_complete(async_mock_mqtt_component(hass)) - def tearDown(self): # pylint: disable=invalid-name - """Stop down everything that was started.""" - self.hass.stop() - def test_default_supported_features(self): - """Test that the correct supported features.""" - assert setup_component(self.hass, vacuum.DOMAIN, { - vacuum.DOMAIN: self.default_config, - }) - entity = self.hass.states.get('vacuum.mqtttest') - entity_features = \ - entity.attributes.get(mqtt.CONF_SUPPORTED_FEATURES, 0) - assert sorted(mqtt.services_to_strings(entity_features)) == \ - sorted(['turn_on', 'turn_off', 'stop', - 'return_home', 'battery', 'status', - 'clean_spot']) +async def test_default_supported_features(hass, mock_publish): + """Test that the correct supported features.""" + assert await async_setup_component(hass, vacuum.DOMAIN, { + vacuum.DOMAIN: default_config, + }) + entity = hass.states.get('vacuum.mqtttest') + entity_features = \ + entity.attributes.get(mqttvacuum.CONF_SUPPORTED_FEATURES, 0) + assert sorted(mqttvacuum.services_to_strings(entity_features)) == \ + sorted(['turn_on', 'turn_off', 'stop', + 'return_home', 'battery', 'status', + 'clean_spot']) - def test_all_commands(self): - """Test simple commands to the vacuum.""" - self.default_config[mqtt.CONF_SUPPORTED_FEATURES] = \ - mqtt.services_to_strings(mqtt.ALL_SERVICES) - assert setup_component(self.hass, vacuum.DOMAIN, { - vacuum.DOMAIN: self.default_config, - }) +async def test_all_commands(hass, mock_publish): + """Test simple commands to the vacuum.""" + default_config[mqttvacuum.CONF_SUPPORTED_FEATURES] = \ + mqttvacuum.services_to_strings(mqttvacuum.ALL_SERVICES) - common.turn_on(self.hass, 'vacuum.mqtttest') - self.hass.block_till_done() - self.mock_publish.async_publish.assert_called_once_with( - 'vacuum/command', 'turn_on', 0, False) - self.mock_publish.async_publish.reset_mock() + assert await async_setup_component(hass, vacuum.DOMAIN, { + vacuum.DOMAIN: default_config, + }) - common.turn_off(self.hass, 'vacuum.mqtttest') - self.hass.block_till_done() - self.mock_publish.async_publish.assert_called_once_with( - 'vacuum/command', 'turn_off', 0, False) - self.mock_publish.async_publish.reset_mock() + common.turn_on(hass, 'vacuum.mqtttest') + await hass.async_block_till_done() + await hass.async_block_till_done() + mock_publish.async_publish.assert_called_once_with( + 'vacuum/command', 'turn_on', 0, False) + mock_publish.async_publish.reset_mock() - common.stop(self.hass, 'vacuum.mqtttest') - self.hass.block_till_done() - self.mock_publish.async_publish.assert_called_once_with( - 'vacuum/command', 'stop', 0, False) - self.mock_publish.async_publish.reset_mock() + common.turn_off(hass, 'vacuum.mqtttest') + await hass.async_block_till_done() + await hass.async_block_till_done() + mock_publish.async_publish.assert_called_once_with( + 'vacuum/command', 'turn_off', 0, False) + mock_publish.async_publish.reset_mock() - common.clean_spot(self.hass, 'vacuum.mqtttest') - self.hass.block_till_done() - self.mock_publish.async_publish.assert_called_once_with( - 'vacuum/command', 'clean_spot', 0, False) - self.mock_publish.async_publish.reset_mock() + common.stop(hass, 'vacuum.mqtttest') + await hass.async_block_till_done() + await hass.async_block_till_done() + mock_publish.async_publish.assert_called_once_with( + 'vacuum/command', 'stop', 0, False) + mock_publish.async_publish.reset_mock() - common.locate(self.hass, 'vacuum.mqtttest') - self.hass.block_till_done() - self.mock_publish.async_publish.assert_called_once_with( - 'vacuum/command', 'locate', 0, False) - self.mock_publish.async_publish.reset_mock() + common.clean_spot(hass, 'vacuum.mqtttest') + await hass.async_block_till_done() + await hass.async_block_till_done() + mock_publish.async_publish.assert_called_once_with( + 'vacuum/command', 'clean_spot', 0, False) + mock_publish.async_publish.reset_mock() - common.start_pause(self.hass, 'vacuum.mqtttest') - self.hass.block_till_done() - self.mock_publish.async_publish.assert_called_once_with( - 'vacuum/command', 'start_pause', 0, False) - self.mock_publish.async_publish.reset_mock() + common.locate(hass, 'vacuum.mqtttest') + await hass.async_block_till_done() + await hass.async_block_till_done() + mock_publish.async_publish.assert_called_once_with( + 'vacuum/command', 'locate', 0, False) + mock_publish.async_publish.reset_mock() - common.return_to_base(self.hass, 'vacuum.mqtttest') - self.hass.block_till_done() - self.mock_publish.async_publish.assert_called_once_with( - 'vacuum/command', 'return_to_base', 0, False) - self.mock_publish.async_publish.reset_mock() + common.start_pause(hass, 'vacuum.mqtttest') + await hass.async_block_till_done() + await hass.async_block_till_done() + mock_publish.async_publish.assert_called_once_with( + 'vacuum/command', 'start_pause', 0, False) + mock_publish.async_publish.reset_mock() - common.set_fan_speed(self.hass, 'high', 'vacuum.mqtttest') - self.hass.block_till_done() - self.mock_publish.async_publish.assert_called_once_with( - 'vacuum/set_fan_speed', 'high', 0, False) - self.mock_publish.async_publish.reset_mock() + common.return_to_base(hass, 'vacuum.mqtttest') + await hass.async_block_till_done() + await hass.async_block_till_done() + mock_publish.async_publish.assert_called_once_with( + 'vacuum/command', 'return_to_base', 0, False) + mock_publish.async_publish.reset_mock() - common.send_command(self.hass, '44 FE 93', entity_id='vacuum.mqtttest') - self.hass.block_till_done() - self.mock_publish.async_publish.assert_called_once_with( - 'vacuum/send_command', '44 FE 93', 0, False) + common.set_fan_speed(hass, 'high', 'vacuum.mqtttest') + await hass.async_block_till_done() + await hass.async_block_till_done() + mock_publish.async_publish.assert_called_once_with( + 'vacuum/set_fan_speed', 'high', 0, False) + mock_publish.async_publish.reset_mock() - def test_status(self): - """Test status updates from the vacuum.""" - self.default_config[mqtt.CONF_SUPPORTED_FEATURES] = \ - mqtt.services_to_strings(mqtt.ALL_SERVICES) + common.send_command(hass, '44 FE 93', entity_id='vacuum.mqtttest') + await hass.async_block_till_done() + await hass.async_block_till_done() + mock_publish.async_publish.assert_called_once_with( + 'vacuum/send_command', '44 FE 93', 0, False) - assert setup_component(self.hass, vacuum.DOMAIN, { - vacuum.DOMAIN: self.default_config, - }) - message = """{ - "battery_level": 54, - "cleaning": true, - "docked": false, - "charging": false, - "fan_speed": "max" - }""" - fire_mqtt_message(self.hass, 'vacuum/state', message) - self.hass.block_till_done() - state = self.hass.states.get('vacuum.mqtttest') - assert STATE_ON == state.state - assert 'mdi:battery-50' == \ - state.attributes.get(ATTR_BATTERY_ICON) - assert 54 == state.attributes.get(ATTR_BATTERY_LEVEL) - assert 'max' == state.attributes.get(ATTR_FAN_SPEED) +async def test_status(hass, mock_publish): + """Test status updates from the vacuum.""" + default_config[mqttvacuum.CONF_SUPPORTED_FEATURES] = \ + mqttvacuum.services_to_strings(mqttvacuum.ALL_SERVICES) - message = """{ - "battery_level": 61, - "docked": true, - "cleaning": false, - "charging": true, - "fan_speed": "min" - }""" + assert await async_setup_component(hass, vacuum.DOMAIN, { + vacuum.DOMAIN: default_config, + }) - fire_mqtt_message(self.hass, 'vacuum/state', message) - self.hass.block_till_done() - state = self.hass.states.get('vacuum.mqtttest') - assert STATE_OFF == state.state - assert 'mdi:battery-charging-60' == \ - state.attributes.get(ATTR_BATTERY_ICON) - assert 61 == state.attributes.get(ATTR_BATTERY_LEVEL) - assert 'min' == state.attributes.get(ATTR_FAN_SPEED) + message = """{ + "battery_level": 54, + "cleaning": true, + "docked": false, + "charging": false, + "fan_speed": "max" + }""" + async_fire_mqtt_message(hass, 'vacuum/state', message) + await hass.async_block_till_done() + await hass.async_block_till_done() + state = hass.states.get('vacuum.mqtttest') + assert STATE_ON == state.state + assert 'mdi:battery-50' == \ + state.attributes.get(ATTR_BATTERY_ICON) + assert 54 == state.attributes.get(ATTR_BATTERY_LEVEL) + assert 'max' == state.attributes.get(ATTR_FAN_SPEED) - def test_battery_template(self): - """Test that you can use non-default templates for battery_level.""" - self.default_config.update({ - mqtt.CONF_SUPPORTED_FEATURES: - mqtt.services_to_strings(mqtt.ALL_SERVICES), - mqtt.CONF_BATTERY_LEVEL_TOPIC: "retroroomba/battery_level", - mqtt.CONF_BATTERY_LEVEL_TEMPLATE: "{{ value }}" - }) + message = """{ + "battery_level": 61, + "docked": true, + "cleaning": false, + "charging": true, + "fan_speed": "min" + }""" - assert setup_component(self.hass, vacuum.DOMAIN, { - vacuum.DOMAIN: self.default_config, - }) + async_fire_mqtt_message(hass, 'vacuum/state', message) + await hass.async_block_till_done() + await hass.async_block_till_done() + state = hass.states.get('vacuum.mqtttest') + assert STATE_OFF == state.state + assert 'mdi:battery-charging-60' == \ + state.attributes.get(ATTR_BATTERY_ICON) + assert 61 == state.attributes.get(ATTR_BATTERY_LEVEL) + assert 'min' == state.attributes.get(ATTR_FAN_SPEED) - fire_mqtt_message(self.hass, 'retroroomba/battery_level', '54') - self.hass.block_till_done() - state = self.hass.states.get('vacuum.mqtttest') - assert 54 == state.attributes.get(ATTR_BATTERY_LEVEL) - assert state.attributes.get(ATTR_BATTERY_ICON) == \ - 'mdi:battery-50' - def test_status_invalid_json(self): - """Test to make sure nothing breaks if the vacuum sends bad JSON.""" - self.default_config[mqtt.CONF_SUPPORTED_FEATURES] = \ - mqtt.services_to_strings(mqtt.ALL_SERVICES) +async def test_battery_template(hass, mock_publish): + """Test that you can use non-default templates for battery_level.""" + default_config.update({ + mqttvacuum.CONF_SUPPORTED_FEATURES: + mqttvacuum.services_to_strings(mqttvacuum.ALL_SERVICES), + mqttvacuum.CONF_BATTERY_LEVEL_TOPIC: "retroroomba/battery_level", + mqttvacuum.CONF_BATTERY_LEVEL_TEMPLATE: "{{ value }}" + }) - assert setup_component(self.hass, vacuum.DOMAIN, { - vacuum.DOMAIN: self.default_config, - }) + assert await async_setup_component(hass, vacuum.DOMAIN, { + vacuum.DOMAIN: default_config, + }) - fire_mqtt_message(self.hass, 'vacuum/state', '{"asdfasas false}') - self.hass.block_till_done() - state = self.hass.states.get('vacuum.mqtttest') - assert STATE_OFF == state.state - assert "Stopped" == state.attributes.get(ATTR_STATUS) + async_fire_mqtt_message(hass, 'retroroomba/battery_level', '54') + await hass.async_block_till_done() + state = hass.states.get('vacuum.mqtttest') + assert 54 == state.attributes.get(ATTR_BATTERY_LEVEL) + assert state.attributes.get(ATTR_BATTERY_ICON) == \ + 'mdi:battery-50' - def test_default_availability_payload(self): - """Test availability by default payload with defined topic.""" - self.default_config.update({ - 'availability_topic': 'availability-topic' - }) - assert setup_component(self.hass, vacuum.DOMAIN, { - vacuum.DOMAIN: self.default_config, - }) +async def test_status_invalid_json(hass, mock_publish): + """Test to make sure nothing breaks if the vacuum sends bad JSON.""" + default_config[mqttvacuum.CONF_SUPPORTED_FEATURES] = \ + mqttvacuum.services_to_strings(mqttvacuum.ALL_SERVICES) - state = self.hass.states.get('vacuum.mqtttest') - assert STATE_UNAVAILABLE == state.state + assert await async_setup_component(hass, vacuum.DOMAIN, { + vacuum.DOMAIN: default_config, + }) - fire_mqtt_message(self.hass, 'availability-topic', 'online') - self.hass.block_till_done() + async_fire_mqtt_message(hass, 'vacuum/state', '{"asdfasas false}') + await hass.async_block_till_done() + state = hass.states.get('vacuum.mqtttest') + assert STATE_OFF == state.state + assert "Stopped" == state.attributes.get(ATTR_STATUS) - state = self.hass.states.get('vacuum.mqtttest') - assert STATE_UNAVAILABLE != state.state - fire_mqtt_message(self.hass, 'availability-topic', 'offline') - self.hass.block_till_done() +async def test_default_availability_payload(hass, mock_publish): + """Test availability by default payload with defined topic.""" + default_config.update({ + 'availability_topic': 'availability-topic' + }) - state = self.hass.states.get('vacuum.mqtttest') - assert STATE_UNAVAILABLE == state.state + assert await async_setup_component(hass, vacuum.DOMAIN, { + vacuum.DOMAIN: default_config, + }) - def test_custom_availability_payload(self): - """Test availability by custom payload with defined topic.""" - self.default_config.update({ - 'availability_topic': 'availability-topic', - 'payload_available': 'good', - 'payload_not_available': 'nogood' - }) + state = hass.states.get('vacuum.mqtttest') + assert STATE_UNAVAILABLE == state.state - assert setup_component(self.hass, vacuum.DOMAIN, { - vacuum.DOMAIN: self.default_config, - }) + async_fire_mqtt_message(hass, 'availability-topic', 'online') + await hass.async_block_till_done() + await hass.async_block_till_done() - state = self.hass.states.get('vacuum.mqtttest') - assert STATE_UNAVAILABLE == state.state + state = hass.states.get('vacuum.mqtttest') + assert STATE_UNAVAILABLE != state.state - fire_mqtt_message(self.hass, 'availability-topic', 'good') - self.hass.block_till_done() + async_fire_mqtt_message(hass, 'availability-topic', 'offline') + await hass.async_block_till_done() + await hass.async_block_till_done() - state = self.hass.states.get('vacuum.mqtttest') - assert STATE_UNAVAILABLE != state.state + state = hass.states.get('vacuum.mqtttest') + assert STATE_UNAVAILABLE == state.state - fire_mqtt_message(self.hass, 'availability-topic', 'nogood') - self.hass.block_till_done() - state = self.hass.states.get('vacuum.mqtttest') - assert STATE_UNAVAILABLE == state.state +async def test_custom_availability_payload(hass, mock_publish): + """Test availability by custom payload with defined topic.""" + default_config.update({ + 'availability_topic': 'availability-topic', + 'payload_available': 'good', + 'payload_not_available': 'nogood' + }) + + assert await async_setup_component(hass, vacuum.DOMAIN, { + vacuum.DOMAIN: default_config, + }) + + state = hass.states.get('vacuum.mqtttest') + assert STATE_UNAVAILABLE == state.state + + async_fire_mqtt_message(hass, 'availability-topic', 'good') + await hass.async_block_till_done() + await hass.async_block_till_done() + + state = hass.states.get('vacuum.mqtttest') + assert STATE_UNAVAILABLE != state.state + + async_fire_mqtt_message(hass, 'availability-topic', 'nogood') + await hass.async_block_till_done() + await hass.async_block_till_done() + + state = hass.states.get('vacuum.mqtttest') + assert STATE_UNAVAILABLE == state.state + + +async def test_discovery_removal_vacuum(hass, mock_publish): + """Test removal of discovered vacuum.""" + entry = MockConfigEntry(domain=mqtt.DOMAIN) + await async_start(hass, 'homeassistant', {}, entry) + + data = ( + '{ "name": "Beer",' + ' "command_topic": "test_topic" }' + ) + + async_fire_mqtt_message(hass, 'homeassistant/vacuum/bla/config', + data) + await hass.async_block_till_done() + await hass.async_block_till_done() + + state = hass.states.get('vacuum.beer') + assert state is not None + assert state.name == 'Beer' + + async_fire_mqtt_message(hass, 'homeassistant/vacuum/bla/config', '') + await hass.async_block_till_done() + await hass.async_block_till_done() + + state = hass.states.get('vacuum.beer') + assert state is None + + +async def test_discovery_update_vacuum(hass, mock_publish): + """Test update of discovered vacuum.""" + entry = MockConfigEntry(domain=mqtt.DOMAIN) + await async_start(hass, 'homeassistant', {}, entry) + + data1 = ( + '{ "name": "Beer",' + ' "command_topic": "test_topic" }' + ) + data2 = ( + '{ "name": "Milk",' + ' "command_topic": "test_topic" }' + ) + + async_fire_mqtt_message(hass, 'homeassistant/vacuum/bla/config', + data1) + await hass.async_block_till_done() + + state = hass.states.get('vacuum.beer') + assert state is not None + assert state.name == 'Beer' + + async_fire_mqtt_message(hass, 'homeassistant/vacuum/bla/config', + data2) + await hass.async_block_till_done() + await hass.async_block_till_done() + + state = hass.states.get('vacuum.beer') + assert state is not None + assert state.name == 'Milk' + state = hass.states.get('vacuum.milk') + assert state is None From 3ffa0176cc11b8860857b311f9b1b32d57978ce9 Mon Sep 17 00:00:00 2001 From: Johann Kellerman Date: Sun, 6 Jan 2019 19:20:19 +0200 Subject: [PATCH 019/235] SMA sensor - updated library (#19753) --- homeassistant/components/sensor/sma.py | 39 ++++++++++++++++++-------- requirements_all.txt | 2 +- 2 files changed, 28 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/sensor/sma.py b/homeassistant/components/sensor/sma.py index acf1ead186c..4bfa62bf6dd 100644 --- a/homeassistant/components/sensor/sma.py +++ b/homeassistant/components/sensor/sma.py @@ -19,7 +19,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_time_interval -REQUIREMENTS = ['pysma==0.2.2'] +REQUIREMENTS = ['pysma==0.3.1'] _LOGGER = logging.getLogger(__name__) @@ -37,11 +37,13 @@ def _check_sensor_schema(conf): """Check sensors and attributes are valid.""" try: import pysma - valid = [s.name for s in pysma.SENSORS] + valid = [s.name for s in pysma.Sensors()] except (ImportError, AttributeError): return conf - valid.extend(conf[CONF_CUSTOM].keys()) + for name in conf[CONF_CUSTOM]: + valid.append(name) + for sname, attrs in conf[CONF_SENSORS].items(): if sname not in valid: raise vol.Invalid("{} does not exist".format(sname)) @@ -65,7 +67,8 @@ PLATFORM_SCHEMA = vol.All(PLATFORM_SCHEMA.extend({ vol.Optional(CONF_VERIFY_SSL, default=True): cv.boolean, vol.Required(CONF_PASSWORD): cv.string, vol.Optional(CONF_GROUP, default=GROUPS[0]): vol.In(GROUPS), - vol.Required(CONF_SENSORS): vol.Schema({cv.slug: cv.ensure_list}), + vol.Optional(CONF_SENSORS, default={}): + vol.Schema({cv.slug: cv.ensure_list}), vol.Optional(CONF_CUSTOM, default={}): vol.Schema({cv.slug: CUSTOM_SCHEMA}), }, extra=vol.PREVENT_EXTRA), _check_sensor_schema) @@ -79,22 +82,29 @@ async def async_setup_platform( # Check config again during load - dependency available config = _check_sensor_schema(config) - # Sensor_defs from the custom config - for name, prop in config[CONF_CUSTOM].items(): - n_s = pysma.Sensor(name, prop['key'], prop['unit'], prop['factor']) - pysma.add_sensor(n_s) + # Init all default sensors + sensor_def = pysma.Sensors() + + # Sensor from the custom config + sensor_def.add([pysma.Sensor(o[CONF_KEY], n, o[CONF_UNIT], o[CONF_FACTOR]) + for n, o in config[CONF_CUSTOM].items()]) + + # Use all sensors by default + config_sensors = config[CONF_SENSORS] + if not config_sensors: + config_sensors = {s.name: [] for s in sensor_def} # Prepare all HASS sensor entities hass_sensors = [] used_sensors = [] - for name, attr in config[CONF_SENSORS].items(): - sub_sensors = [pysma.get_sensor(s) for s in attr] - hass_sensors.append(SMAsensor(pysma.get_sensor(name), sub_sensors)) + for name, attr in config_sensors.items(): + sub_sensors = [sensor_def[s] for s in attr] + hass_sensors.append(SMAsensor(sensor_def[name], sub_sensors)) used_sensors.append(name) used_sensors.extend(attr) async_add_entities(hass_sensors) - used_sensors = [pysma.get_sensor(s) for s in set(used_sensors)] + used_sensors = [sensor_def[s] for s in set(used_sensors)] # Init the SMA interface session = async_get_clientsession(hass, verify_ssl=config[CONF_VERIFY_SSL]) @@ -195,3 +205,8 @@ class SMAsensor(Entity): self._state = self._sensor.value return self.async_update_ha_state() if update else None + + @property + def unique_id(self): + """Return a unique identifier for this sensor.""" + return "sma-{}-{}".format(self._sensor.key, self._sensor.name) diff --git a/requirements_all.txt b/requirements_all.txt index 3f98fb7e2e1..7f162434bd7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1198,7 +1198,7 @@ pysesame==0.1.0 pysher==1.0.1 # homeassistant.components.sensor.sma -pysma==0.2.2 +pysma==0.3.1 # homeassistant.components.device_tracker.snmp # homeassistant.components.sensor.snmp From bf4830bc077a47edd8c241c584e373030590bcaf Mon Sep 17 00:00:00 2001 From: cdheiser <10488026+cdheiser@users.noreply.github.com> Date: Sun, 6 Jan 2019 09:25:09 -0800 Subject: [PATCH 020/235] Fix a bug in Lutron RadioRA2 Scene support (#19819) --- homeassistant/components/lutron.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/lutron.py b/homeassistant/components/lutron.py index 48ad95da415..7f1e1d25ae1 100644 --- a/homeassistant/components/lutron.py +++ b/homeassistant/components/lutron.py @@ -68,7 +68,7 @@ def setup(hass, base_config): button.name != 'Unknown Button' and button.button_type in ('SingleAction', 'Toggle')): hass.data[LUTRON_DEVICES]['scene'].append( - (area.name, button, led)) + (area.name, keypad.name, button, led)) for component in ('light', 'cover', 'switch', 'scene'): discovery.load_platform(hass, component, DOMAIN, None, base_config) From 5b35317e1e9f3fe28298cd862525c6a386f267df Mon Sep 17 00:00:00 2001 From: Tommy Jonsson Date: Sun, 6 Jan 2019 19:23:33 +0100 Subject: [PATCH 021/235] [3/3] mqtt-vacuum device-registry (#19479) * add device registry to mqtt-vacuum --- homeassistant/components/vacuum/mqtt.py | 20 ++++++-- tests/components/vacuum/test_mqtt.py | 65 ++++++++++++++++++++++++- 2 files changed, 81 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/vacuum/mqtt.py b/homeassistant/components/vacuum/mqtt.py index eeed7090ebf..bc48362b511 100644 --- a/homeassistant/components/vacuum/mqtt.py +++ b/homeassistant/components/vacuum/mqtt.py @@ -11,14 +11,15 @@ import voluptuous as vol from homeassistant.components import mqtt from homeassistant.components.mqtt import ( ATTR_DISCOVERY_HASH, MqttAvailability, MqttDiscoveryUpdate, - subscription) + MqttEntityDeviceInfo, subscription) from homeassistant.components.mqtt.discovery import MQTT_DISCOVERY_NEW from homeassistant.components.vacuum import ( SUPPORT_BATTERY, SUPPORT_CLEAN_SPOT, SUPPORT_FAN_SPEED, SUPPORT_LOCATE, SUPPORT_PAUSE, SUPPORT_RETURN_HOME, SUPPORT_SEND_COMMAND, SUPPORT_STATUS, SUPPORT_STOP, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, VacuumDevice, DOMAIN) -from homeassistant.const import ATTR_SUPPORTED_FEATURES, CONF_NAME +from homeassistant.const import ( + ATTR_SUPPORTED_FEATURES, CONF_NAME, CONF_DEVICE) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -93,6 +94,7 @@ CONF_FAN_SPEED_TEMPLATE = 'fan_speed_template' CONF_SET_FAN_SPEED_TOPIC = 'set_fan_speed_topic' CONF_FAN_SPEED_LIST = 'fan_speed_list' CONF_SEND_COMMAND_TOPIC = 'send_command_topic' +CONF_UNIQUE_ID = 'unique_id' DEFAULT_NAME = 'MQTT Vacuum' DEFAULT_RETAIN = False @@ -143,6 +145,8 @@ PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({ vol.Optional(CONF_FAN_SPEED_LIST, default=[]): vol.All(cv.ensure_list, [cv.string]), vol.Optional(CONF_SEND_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_UNIQUE_ID): cv.string, + vol.Optional(CONF_DEVICE): mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA, }).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) @@ -171,7 +175,9 @@ async def _async_setup_entity(config, async_add_entities, async_add_entities([MqttVacuum(config, discovery_hash)]) -class MqttVacuum(MqttAvailability, MqttDiscoveryUpdate, VacuumDevice): +# pylint: disable=too-many-ancestors +class MqttVacuum(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, + VacuumDevice): """Representation of a MQTT-controlled vacuum.""" def __init__(self, config, discovery_info): @@ -185,6 +191,7 @@ class MqttVacuum(MqttAvailability, MqttDiscoveryUpdate, VacuumDevice): self._fan_speed = 'unknown' self._fan_speed_list = [] self._sub_state = None + self._unique_id = config.get(CONF_UNIQUE_ID) # Load config self._setup_from_config(config) @@ -193,11 +200,13 @@ class MqttVacuum(MqttAvailability, MqttDiscoveryUpdate, VacuumDevice): availability_topic = config.get(mqtt.CONF_AVAILABILITY_TOPIC) payload_available = config.get(mqtt.CONF_PAYLOAD_AVAILABLE) payload_not_available = config.get(mqtt.CONF_PAYLOAD_NOT_AVAILABLE) + device_config = config.get(CONF_DEVICE) MqttAvailability.__init__(self, availability_topic, qos, payload_available, payload_not_available) MqttDiscoveryUpdate.__init__(self, discovery_info, self.discovery_update) + MqttEntityDeviceInfo.__init__(self, device_config) def _setup_from_config(self, config): self._name = config.get(CONF_NAME) @@ -361,6 +370,11 @@ class MqttVacuum(MqttAvailability, MqttDiscoveryUpdate, VacuumDevice): """Return true if vacuum is on.""" return self._cleaning + @property + def unique_id(self): + """Return a unique ID.""" + return self._unique_id + @property def status(self): """Return a status string for the vacuum.""" diff --git a/tests/components/vacuum/test_mqtt.py b/tests/components/vacuum/test_mqtt.py index 3df6235f85c..9031034ff6e 100644 --- a/tests/components/vacuum/test_mqtt.py +++ b/tests/components/vacuum/test_mqtt.py @@ -1,4 +1,5 @@ """The tests for the Mqtt vacuum platform.""" +import json import pytest from homeassistant.setup import async_setup_component @@ -15,7 +16,6 @@ from tests.common import ( async_fire_mqtt_message, MockConfigEntry) from tests.components.vacuum import common - default_config = { CONF_PLATFORM: 'mqtt', CONF_NAME: 'mqtttest', @@ -330,3 +330,66 @@ async def test_discovery_update_vacuum(hass, mock_publish): assert state.name == 'Milk' state = hass.states.get('vacuum.milk') assert state is None + + +async def test_unique_id(hass, mock_publish): + """Test unique id option only creates one vacuum per unique_id.""" + await async_mock_mqtt_component(hass) + assert await async_setup_component(hass, vacuum.DOMAIN, { + vacuum.DOMAIN: [{ + 'platform': 'mqtt', + 'name': 'Test 1', + 'command_topic': 'command-topic', + 'unique_id': 'TOTALLY_UNIQUE' + }, { + 'platform': 'mqtt', + 'name': 'Test 2', + 'command_topic': 'command-topic', + 'unique_id': 'TOTALLY_UNIQUE' + }] + }) + + async_fire_mqtt_message(hass, 'test-topic', 'payload') + await hass.async_block_till_done() + await hass.async_block_till_done() + + assert len(hass.states.async_entity_ids()) == 2 + # all vacuums group is 1, unique id created is 1 + + +async def test_entity_device_info_with_identifier(hass, mock_publish): + """Test MQTT vacuum device registry integration.""" + entry = MockConfigEntry(domain=mqtt.DOMAIN) + entry.add_to_hass(hass) + await async_start(hass, 'homeassistant', {}, entry) + registry = await hass.helpers.device_registry.async_get_registry() + + data = json.dumps({ + 'platform': 'mqtt', + 'name': 'Test 1', + 'command_topic': 'test-command-topic', + 'device': { + 'identifiers': ['helloworld'], + 'connections': [ + ["mac", "02:5b:26:a8:dc:12"], + ], + 'manufacturer': 'Whatever', + 'name': 'Beer', + 'model': 'Glass', + 'sw_version': '0.1-beta', + }, + 'unique_id': 'veryunique' + }) + async_fire_mqtt_message(hass, 'homeassistant/vacuum/bla/config', + data) + await hass.async_block_till_done() + await hass.async_block_till_done() + + device = registry.async_get_device({('mqtt', 'helloworld')}, set()) + assert device is not None + assert device.identifiers == {('mqtt', 'helloworld')} + assert device.connections == {('mac', "02:5b:26:a8:dc:12")} + assert device.manufacturer == 'Whatever' + assert device.name == 'Beer' + assert device.model == 'Glass' + assert device.sw_version == '0.1-beta' From 1e18a2c6791a60ac61461872f8a5960859ce6c5f Mon Sep 17 00:00:00 2001 From: Rene Nulsch <33263735+ReneNulschDE@users.noreply.github.com> Date: Sun, 6 Jan 2019 20:52:55 +0100 Subject: [PATCH 022/235] Remove temperature from the list of available forecast sensors (#19818) --- homeassistant/components/sensor/darksky.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/darksky.py b/homeassistant/components/sensor/darksky.py index 06284028457..06232baca4e 100644 --- a/homeassistant/components/sensor/darksky.py +++ b/homeassistant/components/sensor/darksky.py @@ -74,7 +74,7 @@ SENSOR_TYPES = { ['hourly', 'daily']], 'temperature': ['Temperature', '°C', '°F', '°C', '°C', '°C', 'mdi:thermometer', - ['currently', 'hourly', 'daily']], + ['currently', 'hourly']], 'apparent_temperature': ['Apparent Temperature', '°C', '°F', '°C', '°C', '°C', 'mdi:thermometer', ['currently', 'hourly']], From c96778c82a21ddaa478436eb2478656e2130aced Mon Sep 17 00:00:00 2001 From: kennedyshead Date: Sun, 6 Jan 2019 21:06:20 +0100 Subject: [PATCH 023/235] This makes the vasttrafik platform stop spamming the logs with warnings (#19792) * This makes the vasttrafik platform stop spamming the logs with warrnings * Forcing build --- homeassistant/components/sensor/vasttrafik.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/vasttrafik.py b/homeassistant/components/sensor/vasttrafik.py index 7ef4170dd5a..afa07f46fde 100644 --- a/homeassistant/components/sensor/vasttrafik.py +++ b/homeassistant/components/sensor/vasttrafik.py @@ -59,6 +59,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): planner = vasttrafik.JournyPlanner( config.get(CONF_KEY), config.get(CONF_SECRET)) sensors = [] + for departure in config.get(CONF_DEPARTURES): sensors.append( VasttrafikDepartureSensor( @@ -116,7 +117,7 @@ class VasttrafikDepartureSensor(Entity): def state(self): """Return the next departure time.""" if not self._departureboard: - _LOGGER.warning( + _LOGGER.debug( "No departures from %s heading %s", self._departure['name'], self._heading['name'] if self._heading else 'ANY') @@ -143,5 +144,5 @@ class VasttrafikDepartureSensor(Entity): direction=self._heading['id'] if self._heading else None, date=datetime.now()+self._delay) except self._vasttrafik.Error: - _LOGGER.warning("Unable to read departure board, updating token") + _LOGGER.debug("Unable to read departure board, updating token") self._planner.update_token() From 903c86a116bd9acda15f7be9761d511d62c16d38 Mon Sep 17 00:00:00 2001 From: Daniel Shokouhi Date: Sun, 6 Jan 2019 14:58:36 -0800 Subject: [PATCH 024/235] Bump pybotvac (#19831) * Bump pybotvac to support No Go lines * Update requirements --- homeassistant/components/neato.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/neato.py b/homeassistant/components/neato.py index 5da70bfdcd2..31410d1c9b2 100644 --- a/homeassistant/components/neato.py +++ b/homeassistant/components/neato.py @@ -17,7 +17,7 @@ from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['pybotvac==0.0.12'] +REQUIREMENTS = ['pybotvac==0.0.13'] DOMAIN = 'neato' NEATO_ROBOTS = 'neato_robots' diff --git a/requirements_all.txt b/requirements_all.txt index 7f162434bd7..e23fa3b63aa 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -919,7 +919,7 @@ pyblackbird==0.5 # pybluez==0.22 # homeassistant.components.neato -pybotvac==0.0.12 +pybotvac==0.0.13 # homeassistant.components.cloudflare pycfdns==0.0.1 From e30c324b32329c607ab98351cf60ff74e5cfb070 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Mon, 7 Jan 2019 11:58:10 +0100 Subject: [PATCH 025/235] Bump aioesphomeapi (#19838) --- homeassistant/components/esphome/__init__.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index 9034818a934..70d92250564 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -31,7 +31,7 @@ if TYPE_CHECKING: ServiceCall DOMAIN = 'esphome' -REQUIREMENTS = ['aioesphomeapi==1.4.0'] +REQUIREMENTS = ['aioesphomeapi==1.4.1'] DISPATCHER_UPDATE_ENTITY = 'esphome_{entry_id}_update_{component_key}_{key}' diff --git a/requirements_all.txt b/requirements_all.txt index e23fa3b63aa..8f276b81a72 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -96,7 +96,7 @@ aioautomatic==0.6.5 aiodns==1.1.1 # homeassistant.components.esphome -aioesphomeapi==1.4.0 +aioesphomeapi==1.4.1 # homeassistant.components.freebox aiofreepybox==0.0.6 From 3c465434cd8428f72b56f22a48bc820161f23b85 Mon Sep 17 00:00:00 2001 From: Fredrik Erlandsson Date: Mon, 7 Jan 2019 13:04:53 +0100 Subject: [PATCH 026/235] fixes #19814, Daikin config setting (#19823) --- homeassistant/components/daikin/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/daikin/__init__.py b/homeassistant/components/daikin/__init__.py index 86ad6c0a160..f16d6a87d55 100644 --- a/homeassistant/components/daikin/__init__.py +++ b/homeassistant/components/daikin/__init__.py @@ -12,7 +12,7 @@ from socket import timeout import async_timeout import voluptuous as vol -from homeassistant.config_entries import ConfigEntry +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_HOSTS import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC @@ -51,12 +51,12 @@ async def async_setup(hass, config): if not hosts: hass.async_create_task( hass.config_entries.flow.async_init( - DOMAIN, context={'source': config.SOURCE_IMPORT})) + DOMAIN, context={'source': SOURCE_IMPORT})) for host in hosts: hass.async_create_task( hass.config_entries.flow.async_init( DOMAIN, - context={'source': config.SOURCE_IMPORT}, + context={'source': SOURCE_IMPORT}, data={ KEY_HOST: host, })) From 8b232e7ce61f4e6914ff97d131b7c4481de73736 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Mon, 7 Jan 2019 13:36:16 +0100 Subject: [PATCH 027/235] Simplify data_key for a stable unique_id because the order of the dict will not be preserved (Closes: #13522) (#19766) --- homeassistant/components/cover/xiaomi_aqara.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/cover/xiaomi_aqara.py b/homeassistant/components/cover/xiaomi_aqara.py index 3ed0a70b1e0..ead2c0e9219 100644 --- a/homeassistant/components/cover/xiaomi_aqara.py +++ b/homeassistant/components/cover/xiaomi_aqara.py @@ -18,9 +18,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): model = device['model'] if model == 'curtain': devices.append(XiaomiGenericCover(device, "Curtain", - {'status': 'status', - 'pos': 'curtain_level'}, - gateway)) + 'status', gateway)) add_entities(devices) @@ -45,20 +43,20 @@ class XiaomiGenericCover(XiaomiDevice, CoverDevice): def close_cover(self, **kwargs): """Close the cover.""" - self._write_to_hub(self._sid, **{self._data_key['status']: 'close'}) + self._write_to_hub(self._sid, **{self._data_key: 'close'}) def open_cover(self, **kwargs): """Open the cover.""" - self._write_to_hub(self._sid, **{self._data_key['status']: 'open'}) + self._write_to_hub(self._sid, **{self._data_key: 'open'}) def stop_cover(self, **kwargs): """Stop the cover.""" - self._write_to_hub(self._sid, **{self._data_key['status']: 'stop'}) + self._write_to_hub(self._sid, **{self._data_key: 'stop'}) def set_cover_position(self, **kwargs): """Move the cover to a specific position.""" position = kwargs.get(ATTR_POSITION) - self._write_to_hub(self._sid, **{self._data_key['pos']: str(position)}) + self._write_to_hub(self._sid, **{ATTR_CURTAIN_LEVEL: str(position)}) def parse_data(self, data, raw_data): """Parse data sent by gateway.""" From dd75c497964cd7fc46a1758610768ee97a2067cb Mon Sep 17 00:00:00 2001 From: emontnemery Date: Mon, 7 Jan 2019 16:57:51 +0100 Subject: [PATCH 028/235] Cleanup if discovered mqtt switch can't be added (#19721) * Cleanup if discovered mqtt switch can't be added --- homeassistant/components/mqtt/__init__.py | 4 +-- homeassistant/components/mqtt/discovery.py | 5 +++ homeassistant/components/switch/mqtt.py | 15 ++++++--- tests/components/switch/test_mqtt.py | 37 ++++++++++++++++++++-- 4 files changed, 53 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 2750f1c1585..c740086ba2f 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -972,7 +972,7 @@ class MqttDiscoveryUpdate(Entity): from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.components.mqtt.discovery import ( - ALREADY_DISCOVERED, MQTT_DISCOVERY_UPDATED) + MQTT_DISCOVERY_UPDATED, clear_discovery_hash) @callback def discovery_callback(payload): @@ -983,7 +983,7 @@ class MqttDiscoveryUpdate(Entity): # Empty payload: Remove component _LOGGER.info("Removing component: %s", self.entity_id) self.hass.async_create_task(self.async_remove()) - del self.hass.data[ALREADY_DISCOVERED][self._discovery_hash] + clear_discovery_hash(self.hass, self._discovery_hash) self._remove_signal() elif self._discovery_update: # Non-empty payload: Notify component diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py index 790007dc0e8..fc8b9091763 100644 --- a/homeassistant/components/mqtt/discovery.py +++ b/homeassistant/components/mqtt/discovery.py @@ -180,6 +180,11 @@ ABBREVIATIONS = { } +def clear_discovery_hash(hass, discovery_hash): + """Clear entry in ALREADY_DISCOVERED list.""" + del hass.data[ALREADY_DISCOVERED][discovery_hash] + + async def async_start(hass: HomeAssistantType, discovery_topic, hass_config, config_entry=None) -> bool: """Initialize of MQTT Discovery.""" diff --git a/homeassistant/components/switch/mqtt.py b/homeassistant/components/switch/mqtt.py index 494156ea8de..02f0a60aed2 100644 --- a/homeassistant/components/switch/mqtt.py +++ b/homeassistant/components/switch/mqtt.py @@ -14,7 +14,8 @@ from homeassistant.components.mqtt import ( CONF_AVAILABILITY_TOPIC, CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, CONF_RETAIN, MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, subscription) -from homeassistant.components.mqtt.discovery import MQTT_DISCOVERY_NEW +from homeassistant.components.mqtt.discovery import ( + MQTT_DISCOVERY_NEW, clear_discovery_hash) from homeassistant.components.switch import SwitchDevice from homeassistant.const import ( CONF_NAME, CONF_OPTIMISTIC, CONF_VALUE_TEMPLATE, CONF_PAYLOAD_OFF, @@ -61,9 +62,15 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Set up MQTT switch dynamically through MQTT discovery.""" async def async_discover(discovery_payload): """Discover and add a MQTT switch.""" - config = PLATFORM_SCHEMA(discovery_payload) - await _async_setup_entity(config, async_add_entities, - discovery_payload[ATTR_DISCOVERY_HASH]) + try: + discovery_hash = discovery_payload[ATTR_DISCOVERY_HASH] + config = PLATFORM_SCHEMA(discovery_payload) + await _async_setup_entity(config, async_add_entities, + discovery_hash) + except Exception: + if discovery_hash: + clear_discovery_hash(hass, discovery_hash) + raise async_dispatcher_connect( hass, MQTT_DISCOVERY_NEW.format(switch.DOMAIN, 'mqtt'), diff --git a/tests/components/switch/test_mqtt.py b/tests/components/switch/test_mqtt.py index c1aa7c3a212..f5adb4062c6 100644 --- a/tests/components/switch/test_mqtt.py +++ b/tests/components/switch/test_mqtt.py @@ -285,7 +285,7 @@ async def test_unique_id(hass): async def test_discovery_removal_switch(hass, mqtt_mock, caplog): - """Test expansion of discovered switch.""" + """Test removal of discovered switch.""" entry = MockConfigEntry(domain=mqtt.DOMAIN) await async_start(hass, 'homeassistant', {}, entry) @@ -314,7 +314,7 @@ async def test_discovery_removal_switch(hass, mqtt_mock, caplog): async def test_discovery_update_switch(hass, mqtt_mock, caplog): - """Test expansion of discovered switch.""" + """Test update of discovered switch.""" entry = MockConfigEntry(domain=mqtt.DOMAIN) await async_start(hass, 'homeassistant', {}, entry) @@ -349,6 +349,39 @@ async def test_discovery_update_switch(hass, mqtt_mock, caplog): assert state is None +async def test_discovery_broken(hass, mqtt_mock, caplog): + """Test handling of bad discovery message.""" + entry = MockConfigEntry(domain=mqtt.DOMAIN) + await async_start(hass, 'homeassistant', {}, entry) + + data1 = ( + '{ "name": "Beer" }' + ) + data2 = ( + '{ "name": "Milk",' + ' "status_topic": "test_topic",' + ' "command_topic": "test_topic" }' + ) + + async_fire_mqtt_message(hass, 'homeassistant/switch/bla/config', + data1) + await hass.async_block_till_done() + + state = hass.states.get('switch.beer') + assert state is None + + async_fire_mqtt_message(hass, 'homeassistant/switch/bla/config', + data2) + await hass.async_block_till_done() + await hass.async_block_till_done() + + state = hass.states.get('switch.milk') + assert state is not None + assert state.name == 'Milk' + state = hass.states.get('switch.beer') + assert state is None + + async def test_entity_device_info_with_identifier(hass, mqtt_mock): """Test MQTT switch device registry integration.""" entry = MockConfigEntry(domain=mqtt.DOMAIN) From d8370f44cbaae7375482c8b5d8bfb50c222144e4 Mon Sep 17 00:00:00 2001 From: emontnemery Date: Thu, 3 Jan 2019 15:48:36 +0100 Subject: [PATCH 029/235] Cleanup if discovered mqtt binary_sensor can't be added --- .../components/binary_sensor/mqtt.py | 15 +++++--- tests/components/binary_sensor/test_mqtt.py | 35 ++++++++++++++++++- 2 files changed, 45 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/binary_sensor/mqtt.py b/homeassistant/components/binary_sensor/mqtt.py index 0a09d051192..6ad0f23976f 100644 --- a/homeassistant/components/binary_sensor/mqtt.py +++ b/homeassistant/components/binary_sensor/mqtt.py @@ -20,7 +20,8 @@ from homeassistant.components.mqtt import ( CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, subscription) -from homeassistant.components.mqtt.discovery import MQTT_DISCOVERY_NEW +from homeassistant.components.mqtt.discovery import ( + ALREADY_DISCOVERED, MQTT_DISCOVERY_NEW) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect import homeassistant.helpers.event as evt @@ -63,9 +64,15 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Set up MQTT binary sensor dynamically through MQTT discovery.""" async def async_discover(discovery_payload): """Discover and add a MQTT binary sensor.""" - config = PLATFORM_SCHEMA(discovery_payload) - await _async_setup_entity(config, async_add_entities, - discovery_payload[ATTR_DISCOVERY_HASH]) + try: + discovery_hash = discovery_payload[ATTR_DISCOVERY_HASH] + config = PLATFORM_SCHEMA(discovery_payload) + await _async_setup_entity(config, async_add_entities, + discovery_hash) + except: # noqa: E722 + if discovery_hash: + del hass.data[ALREADY_DISCOVERED][discovery_hash] + raise async_dispatcher_connect( hass, MQTT_DISCOVERY_NEW.format(binary_sensor.DOMAIN, 'mqtt'), diff --git a/tests/components/binary_sensor/test_mqtt.py b/tests/components/binary_sensor/test_mqtt.py index 71a30e6ee18..5a1c80beae2 100644 --- a/tests/components/binary_sensor/test_mqtt.py +++ b/tests/components/binary_sensor/test_mqtt.py @@ -390,7 +390,7 @@ async def test_discovery_removal_binary_sensor(hass, mqtt_mock, caplog): async def test_discovery_update_binary_sensor(hass, mqtt_mock, caplog): - """Test removal of discovered binary_sensor.""" + """Test update of discovered binary_sensor.""" entry = MockConfigEntry(domain=mqtt.DOMAIN) await async_start(hass, 'homeassistant', {}, entry) data1 = ( @@ -421,6 +421,39 @@ async def test_discovery_update_binary_sensor(hass, mqtt_mock, caplog): assert state is None +async def test_discovery_broken(hass, mqtt_mock, caplog): + """Test handling of bad discovery message.""" + entry = MockConfigEntry(domain=mqtt.DOMAIN) + await async_start(hass, 'homeassistant', {}, entry) + + data1 = ( + '{ "name": "Beer",' + ' "off_delay": -1 }' + ) + data2 = ( + '{ "name": "Milk",' + ' "state_topic": "test_topic" }' + ) + + async_fire_mqtt_message(hass, 'homeassistant/binary_sensor/bla/config', + data1) + await hass.async_block_till_done() + + state = hass.states.get('binary_sensor.beer') + assert state is None + + async_fire_mqtt_message(hass, 'homeassistant/binary_sensor/bla/config', + data2) + await hass.async_block_till_done() + await hass.async_block_till_done() + + state = hass.states.get('binary_sensor.milk') + assert state is not None + assert state.name == 'Milk' + state = hass.states.get('binary_sensor.beer') + assert state is None + + async def test_entity_device_info_with_identifier(hass, mqtt_mock): """Test MQTT binary sensor device registry integration.""" entry = MockConfigEntry(domain=mqtt.DOMAIN) From 44c619a853f8e3ea27f1e49b6ec6ccc1bb185b72 Mon Sep 17 00:00:00 2001 From: emontnemery Date: Sat, 5 Jan 2019 13:43:19 +0100 Subject: [PATCH 030/235] No bare except --- homeassistant/components/binary_sensor/mqtt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/binary_sensor/mqtt.py b/homeassistant/components/binary_sensor/mqtt.py index 6ad0f23976f..34db8b2f3fb 100644 --- a/homeassistant/components/binary_sensor/mqtt.py +++ b/homeassistant/components/binary_sensor/mqtt.py @@ -69,7 +69,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): config = PLATFORM_SCHEMA(discovery_payload) await _async_setup_entity(config, async_add_entities, discovery_hash) - except: # noqa: E722 + except Exception: if discovery_hash: del hass.data[ALREADY_DISCOVERED][discovery_hash] raise From ddeb7f3beadd354ba1c4075de1d888decc6a0b33 Mon Sep 17 00:00:00 2001 From: emontnemery Date: Sat, 5 Jan 2019 13:43:37 +0100 Subject: [PATCH 031/235] Clear ALREADY_DISCOVERED list with helper --- homeassistant/components/binary_sensor/mqtt.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/binary_sensor/mqtt.py b/homeassistant/components/binary_sensor/mqtt.py index 34db8b2f3fb..f42feec6838 100644 --- a/homeassistant/components/binary_sensor/mqtt.py +++ b/homeassistant/components/binary_sensor/mqtt.py @@ -21,7 +21,7 @@ from homeassistant.components.mqtt import ( MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, subscription) from homeassistant.components.mqtt.discovery import ( - ALREADY_DISCOVERED, MQTT_DISCOVERY_NEW) + MQTT_DISCOVERY_NEW, clear_discovery_hash) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect import homeassistant.helpers.event as evt @@ -71,7 +71,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): discovery_hash) except Exception: if discovery_hash: - del hass.data[ALREADY_DISCOVERED][discovery_hash] + clear_discovery_hash(hass, discovery_hash) raise async_dispatcher_connect( From 0b57cfb004acf973a5a90b47c3e585afad77c410 Mon Sep 17 00:00:00 2001 From: emontnemery Date: Thu, 3 Jan 2019 15:51:18 +0100 Subject: [PATCH 032/235] Cleanup if discovered mqtt sensor can't be added --- homeassistant/components/sensor/mqtt.py | 15 ++++++++--- tests/components/sensor/test_mqtt.py | 35 ++++++++++++++++++++++++- 2 files changed, 45 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/sensor/mqtt.py b/homeassistant/components/sensor/mqtt.py index 49d090f7e1e..ae4a0f2f1ad 100644 --- a/homeassistant/components/sensor/mqtt.py +++ b/homeassistant/components/sensor/mqtt.py @@ -17,7 +17,8 @@ from homeassistant.components.mqtt import ( ATTR_DISCOVERY_HASH, CONF_AVAILABILITY_TOPIC, CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, CONF_STATE_TOPIC, MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, subscription) -from homeassistant.components.mqtt.discovery import MQTT_DISCOVERY_NEW +from homeassistant.components.mqtt.discovery import ( + ALREADY_DISCOVERED, MQTT_DISCOVERY_NEW) from homeassistant.components.sensor import DEVICE_CLASSES_SCHEMA from homeassistant.const import ( CONF_FORCE_UPDATE, CONF_NAME, CONF_VALUE_TEMPLATE, STATE_UNKNOWN, @@ -66,9 +67,15 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Set up MQTT sensors dynamically through MQTT discovery.""" async def async_discover_sensor(discovery_payload): """Discover and add a discovered MQTT sensor.""" - config = PLATFORM_SCHEMA(discovery_payload) - await _async_setup_entity(config, async_add_entities, - discovery_payload[ATTR_DISCOVERY_HASH]) + try: + discovery_hash = discovery_payload[ATTR_DISCOVERY_HASH] + config = PLATFORM_SCHEMA(discovery_payload) + await _async_setup_entity(config, async_add_entities, + discovery_hash) + except: # noqa: E722 + if discovery_hash: + del hass.data[ALREADY_DISCOVERED][discovery_hash] + raise async_dispatcher_connect(hass, MQTT_DISCOVERY_NEW.format(sensor.DOMAIN, 'mqtt'), diff --git a/tests/components/sensor/test_mqtt.py b/tests/components/sensor/test_mqtt.py index 79ba2c7a512..739e81258c2 100644 --- a/tests/components/sensor/test_mqtt.py +++ b/tests/components/sensor/test_mqtt.py @@ -474,7 +474,7 @@ async def test_discovery_removal_sensor(hass, mqtt_mock, caplog): async def test_discovery_update_sensor(hass, mqtt_mock, caplog): - """Test removal of discovered sensor.""" + """Test update of discovered sensor.""" entry = MockConfigEntry(domain=mqtt.DOMAIN) await async_start(hass, 'homeassistant', {}, entry) data1 = ( @@ -506,6 +506,39 @@ async def test_discovery_update_sensor(hass, mqtt_mock, caplog): assert state is None +async def test_discovery_broken(hass, mqtt_mock, caplog): + """Test handling of bad discovery message.""" + entry = MockConfigEntry(domain=mqtt.DOMAIN) + await async_start(hass, 'homeassistant', {}, entry) + + data1 = ( + '{ "name": "Beer",' + ' "state_topic": "test_topic#" }' + ) + data2 = ( + '{ "name": "Milk",' + ' "state_topic": "test_topic" }' + ) + + async_fire_mqtt_message(hass, 'homeassistant/sensor/bla/config', + data1) + await hass.async_block_till_done() + + state = hass.states.get('sensor.beer') + assert state is None + + async_fire_mqtt_message(hass, 'homeassistant/sensor/bla/config', + data2) + await hass.async_block_till_done() + await hass.async_block_till_done() + + state = hass.states.get('sensor.milk') + assert state is not None + assert state.name == 'Milk' + state = hass.states.get('sensor.beer') + assert state is None + + async def test_entity_device_info_with_identifier(hass, mqtt_mock): """Test MQTT sensor device registry integration.""" entry = MockConfigEntry(domain=mqtt.DOMAIN) From 8701be095b967fd3283761031f1d3016e54862d0 Mon Sep 17 00:00:00 2001 From: emontnemery Date: Sat, 5 Jan 2019 14:01:58 +0100 Subject: [PATCH 033/235] No bare except --- homeassistant/components/sensor/mqtt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/mqtt.py b/homeassistant/components/sensor/mqtt.py index ae4a0f2f1ad..25711ce39d6 100644 --- a/homeassistant/components/sensor/mqtt.py +++ b/homeassistant/components/sensor/mqtt.py @@ -72,7 +72,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): config = PLATFORM_SCHEMA(discovery_payload) await _async_setup_entity(config, async_add_entities, discovery_hash) - except: # noqa: E722 + except Exception: if discovery_hash: del hass.data[ALREADY_DISCOVERED][discovery_hash] raise From 08ac6da8a6f52934e1d8f2472cb49dcc2b8a7a50 Mon Sep 17 00:00:00 2001 From: emontnemery Date: Sat, 5 Jan 2019 14:07:00 +0100 Subject: [PATCH 034/235] Clear ALREADY_DISCOVERED list with helper --- homeassistant/components/sensor/mqtt.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/mqtt.py b/homeassistant/components/sensor/mqtt.py index 25711ce39d6..b78ebb048ad 100644 --- a/homeassistant/components/sensor/mqtt.py +++ b/homeassistant/components/sensor/mqtt.py @@ -18,7 +18,7 @@ from homeassistant.components.mqtt import ( CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, CONF_STATE_TOPIC, MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, subscription) from homeassistant.components.mqtt.discovery import ( - ALREADY_DISCOVERED, MQTT_DISCOVERY_NEW) + MQTT_DISCOVERY_NEW, clear_discovery_hash) from homeassistant.components.sensor import DEVICE_CLASSES_SCHEMA from homeassistant.const import ( CONF_FORCE_UPDATE, CONF_NAME, CONF_VALUE_TEMPLATE, STATE_UNKNOWN, @@ -74,7 +74,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): discovery_hash) except Exception: if discovery_hash: - del hass.data[ALREADY_DISCOVERED][discovery_hash] + clear_discovery_hash(hass, discovery_hash) raise async_dispatcher_connect(hass, From 0af635e8d7b64117fc742c63a2ce3f847d3e529c Mon Sep 17 00:00:00 2001 From: sander76 Date: Mon, 7 Jan 2019 17:32:28 +0100 Subject: [PATCH 035/235] adding more dimmer components (#19843) * adding more dimmer components * updated library version * updated requirements_test_all --- homeassistant/components/homematicip_cloud/__init__.py | 2 +- homeassistant/components/light/homematicip_cloud.py | 7 +++++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/homematicip_cloud/__init__.py b/homeassistant/components/homematicip_cloud/__init__.py index 597d1660836..700e6274c35 100644 --- a/homeassistant/components/homematicip_cloud/__init__.py +++ b/homeassistant/components/homematicip_cloud/__init__.py @@ -19,7 +19,7 @@ from .const import ( from .device import HomematicipGenericDevice # noqa: F401 from .hap import HomematicipAuth, HomematicipHAP # noqa: F401 -REQUIREMENTS = ['homematicip==0.10.1'] +REQUIREMENTS = ['homematicip==0.10.3'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/light/homematicip_cloud.py b/homeassistant/components/light/homematicip_cloud.py index 764ead62169..2837edbd5b7 100644 --- a/homeassistant/components/light/homematicip_cloud.py +++ b/homeassistant/components/light/homematicip_cloud.py @@ -29,14 +29,17 @@ async def async_setup_platform( async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the HomematicIP Cloud lights from a config entry.""" - from homematicip.aio.device import AsyncBrandSwitchMeasuring, AsyncDimmer + from homematicip.aio.device import AsyncBrandSwitchMeasuring, AsyncDimmer,\ + AsyncPluggableDimmer, AsyncBrandDimmer, AsyncFullFlushDimmer home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home devices = [] for device in home.devices: if isinstance(device, AsyncBrandSwitchMeasuring): devices.append(HomematicipLightMeasuring(home, device)) - elif isinstance(device, AsyncDimmer): + elif isinstance(device, + (AsyncDimmer, AsyncPluggableDimmer, + AsyncBrandDimmer, AsyncFullFlushDimmer)): devices.append(HomematicipDimmer(home, device)) if devices: diff --git a/requirements_all.txt b/requirements_all.txt index 8f276b81a72..1cc6f37ee0d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -520,7 +520,7 @@ homeassistant-pyozw==0.1.1 # homekit==0.12.0 # homeassistant.components.homematicip_cloud -homematicip==0.10.1 +homematicip==0.10.3 # homeassistant.components.google # homeassistant.components.remember_the_milk diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ecb147c7e81..1429a6332a0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -107,7 +107,7 @@ holidays==0.9.8 home-assistant-frontend==20181219.0 # homeassistant.components.homematicip_cloud -homematicip==0.10.1 +homematicip==0.10.3 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From 9cdfa77a216a06e46790b489b121eaf0bdc2bb4e Mon Sep 17 00:00:00 2001 From: Sean Dague Date: Mon, 7 Jan 2019 11:36:02 -0500 Subject: [PATCH 036/235] bump watefurnace version to 1.1.0 (#19847) There is better retry logic in the new library to handle login faults. --- homeassistant/components/waterfurnace.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/waterfurnace.py b/homeassistant/components/waterfurnace.py index 0947afea141..bbae6170048 100644 --- a/homeassistant/components/waterfurnace.py +++ b/homeassistant/components/waterfurnace.py @@ -19,7 +19,7 @@ from homeassistant.helpers import config_validation as cv from homeassistant.helpers import discovery -REQUIREMENTS = ["waterfurnace==1.0.0"] +REQUIREMENTS = ["waterfurnace==1.1.0"] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 1cc6f37ee0d..82e96c0dcf4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1680,7 +1680,7 @@ warrant==0.6.1 watchdog==0.8.3 # homeassistant.components.waterfurnace -waterfurnace==1.0.0 +waterfurnace==1.1.0 # homeassistant.components.media_player.gpmdp websocket-client==0.54.0 From a57aae9891ea1b4f9ce31d0c5cde0406604e0bcb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Mon, 7 Jan 2019 17:53:31 +0100 Subject: [PATCH 037/235] Fix 2 ResourceWarning: unclosed file in test_ruamel_yaml.py (#19780) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Mickaël Schoentgen --- tests/util/test_ruamel_yaml.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/util/test_ruamel_yaml.py b/tests/util/test_ruamel_yaml.py index 61006c98642..4ac8b85ae98 100644 --- a/tests/util/test_ruamel_yaml.py +++ b/tests/util/test_ruamel_yaml.py @@ -135,7 +135,7 @@ class TestYAML(unittest.TestCase): def test_save_and_load(self): """Test saving and loading back.""" fname = self._path_for("test1") - open(fname, "w+") + open(fname, "w+").close() util_yaml.save_yaml(fname, self.yaml.load(TEST_YAML_A)) data = util_yaml.load_yaml(fname, True) assert data == self.yaml.load(TEST_YAML_A) @@ -143,7 +143,7 @@ class TestYAML(unittest.TestCase): def test_overwrite_and_reload(self): """Test that we can overwrite an existing file and read back.""" fname = self._path_for("test2") - open(fname, "w+") + open(fname, "w+").close() util_yaml.save_yaml(fname, self.yaml.load(TEST_YAML_A)) util_yaml.save_yaml(fname, self.yaml.load(TEST_YAML_B)) data = util_yaml.load_yaml(fname, True) From 2eec2cc656e18ac844c7cefd1886515f7fefbe41 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Mon, 7 Jan 2019 19:00:03 +0100 Subject: [PATCH 038/235] Upgrade holidays to 0.9.9 --- homeassistant/components/binary_sensor/workday.py | 3 ++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/binary_sensor/workday.py b/homeassistant/components/binary_sensor/workday.py index fc8207f83b7..551ca835e78 100644 --- a/homeassistant/components/binary_sensor/workday.py +++ b/homeassistant/components/binary_sensor/workday.py @@ -14,7 +14,7 @@ from homeassistant.const import CONF_NAME, WEEKDAYS from homeassistant.components.binary_sensor import BinarySensorDevice import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['holidays==0.9.8'] +REQUIREMENTS = ['holidays==0.9.9'] _LOGGER = logging.getLogger(__name__) @@ -26,6 +26,7 @@ ALL_COUNTRIES = [ 'Canada', 'CA', 'Colombia', 'CO', 'Croatia', 'HR', 'Czech', 'CZ', 'Denmark', 'DK', 'England', 'EuropeanCentralBank', 'ECB', 'TAR', 'Finland', 'FI', 'France', 'FRA', 'Germany', 'DE', 'Hungary', 'HU', + 'Honduras', 'HUD', 'India', 'IND', 'Ireland', 'Isle of Man', 'Italy', 'IT', 'Japan', 'JP', 'Mexico', 'MX', 'Netherlands', 'NL', 'NewZealand', 'NZ', 'Northern Ireland', 'Norway', 'NO', 'Polish', 'PL', 'Portugal', 'PT', diff --git a/requirements_all.txt b/requirements_all.txt index 82e96c0dcf4..490f9390d93 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -508,7 +508,7 @@ hlk-sw16==0.0.6 hole==0.3.0 # homeassistant.components.binary_sensor.workday -holidays==0.9.8 +holidays==0.9.9 # homeassistant.components.frontend home-assistant-frontend==20181219.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1429a6332a0..7199207f567 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -101,7 +101,7 @@ hbmqtt==0.9.4 hdate==0.7.5 # homeassistant.components.binary_sensor.workday -holidays==0.9.8 +holidays==0.9.9 # homeassistant.components.frontend home-assistant-frontend==20181219.0 From 45fae5a50e796ec4b34c1a0b31fc65fb1759e986 Mon Sep 17 00:00:00 2001 From: koomik Date: Mon, 7 Jan 2019 19:02:42 +0100 Subject: [PATCH 039/235] Upgrade tahoma-api to 0.0.14 (#19840) * Update requirements_all.txt Change to tahoma-api 0.0.14 to solve #19542 https://github.com/home-assistant/home-assistant/issues/19542 * Update tahoma.py --- homeassistant/components/tahoma.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/tahoma.py b/homeassistant/components/tahoma.py index ff9e30e5c68..5e30b845863 100644 --- a/homeassistant/components/tahoma.py +++ b/homeassistant/components/tahoma.py @@ -14,7 +14,7 @@ from homeassistant.helpers import discovery from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['tahoma-api==0.0.13'] +REQUIREMENTS = ['tahoma-api==0.0.14'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 82e96c0dcf4..6b5707719d7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1571,7 +1571,7 @@ suds-py3==1.3.3.0 swisshydrodata==0.0.3 # homeassistant.components.tahoma -tahoma-api==0.0.13 +tahoma-api==0.0.14 # homeassistant.components.sensor.tank_utility tank_utility==1.4.0 From 4905f4dd97e9f2d5cdb4b36b79aa2e82a1c27b23 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Mon, 7 Jan 2019 19:16:04 +0100 Subject: [PATCH 040/235] Upgrade beautifulsoup4 to 4.7.1 --- homeassistant/components/device_tracker/linksys_ap.py | 5 +++-- homeassistant/components/sensor/scrape.py | 2 +- homeassistant/components/sensor/sytadin.py | 2 +- requirements_all.txt | 2 +- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/device_tracker/linksys_ap.py b/homeassistant/components/device_tracker/linksys_ap.py index 5fa33583567..5638db4caaf 100644 --- a/homeassistant/components/device_tracker/linksys_ap.py +++ b/homeassistant/components/device_tracker/linksys_ap.py @@ -19,7 +19,7 @@ from homeassistant.const import ( INTERFACES = 2 DEFAULT_TIMEOUT = 10 -REQUIREMENTS = ['beautifulsoup4==4.6.3'] +REQUIREMENTS = ['beautifulsoup4==4.7.1'] _LOGGER = logging.getLogger(__name__) @@ -81,13 +81,14 @@ class LinksysAPDeviceScanner(DeviceScanner): request = self._make_request(interface) self.last_results.extend( [x.find_all('td')[1].text - for x in BS(request.content, "html.parser") + for x in BS(request.content, 'html.parser') .find_all(class_='section-row')] ) return True def _make_request(self, unit=0): + """Create a request to get the data.""" # No, the '&&' is not a typo - this is expected by the web interface. login = base64.b64encode(bytes(self.username, 'utf8')).decode('ascii') pwd = base64.b64encode(bytes(self.password, 'utf8')).decode('ascii') diff --git a/homeassistant/components/sensor/scrape.py b/homeassistant/components/sensor/scrape.py index 1058d0f43e7..9a67d381d42 100644 --- a/homeassistant/components/sensor/scrape.py +++ b/homeassistant/components/sensor/scrape.py @@ -20,7 +20,7 @@ from homeassistant.helpers.entity import Entity from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['beautifulsoup4==4.6.3'] +REQUIREMENTS = ['beautifulsoup4==4.7.1'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sensor/sytadin.py b/homeassistant/components/sensor/sytadin.py index 18fa73f2341..082342a0393 100644 --- a/homeassistant/components/sensor/sytadin.py +++ b/homeassistant/components/sensor/sytadin.py @@ -18,7 +18,7 @@ from homeassistant.const import ( from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle -REQUIREMENTS = ['beautifulsoup4==4.6.3'] +REQUIREMENTS = ['beautifulsoup4==4.7.1'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 82e96c0dcf4..d58d1e1d89f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -187,7 +187,7 @@ batinfo==0.4.2 # homeassistant.components.device_tracker.linksys_ap # homeassistant.components.sensor.scrape # homeassistant.components.sensor.sytadin -beautifulsoup4==4.6.3 +beautifulsoup4==4.7.1 # homeassistant.components.zha bellows==0.7.0 From 8aa136f7ed6e9bf95fc0324be61157eea879a3b5 Mon Sep 17 00:00:00 2001 From: Lars Lydersen Date: Mon, 7 Jan 2019 20:03:22 +0100 Subject: [PATCH 041/235] Added support for spot cleaning that was introduced in pybotvac 0.12. --- homeassistant/components/vacuum/neato.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/vacuum/neato.py b/homeassistant/components/vacuum/neato.py index 0c97f99df99..8abf2b2779a 100644 --- a/homeassistant/components/vacuum/neato.py +++ b/homeassistant/components/vacuum/neato.py @@ -13,7 +13,7 @@ from homeassistant.components.vacuum import ( SUPPORT_STATE, SUPPORT_STOP, SUPPORT_START, STATE_IDLE, STATE_PAUSED, STATE_CLEANING, STATE_DOCKED, STATE_RETURNING, STATE_ERROR, SUPPORT_MAP, ATTR_STATUS, ATTR_BATTERY_LEVEL, ATTR_BATTERY_ICON, - SUPPORT_LOCATE) + SUPPORT_LOCATE, SUPPORT_CLEAN_SPOT) from homeassistant.components.neato import ( NEATO_ROBOTS, NEATO_LOGIN, NEATO_MAP_DATA, ACTION, ERRORS, MODE, ALERTS) @@ -24,7 +24,7 @@ DEPENDENCIES = ['neato'] SCAN_INTERVAL = timedelta(minutes=5) SUPPORT_NEATO = SUPPORT_BATTERY | SUPPORT_PAUSE | SUPPORT_RETURN_HOME | \ - SUPPORT_STOP | SUPPORT_START | \ + SUPPORT_STOP | SUPPORT_START | SUPPORT_CLEAN_SPOT\ SUPPORT_STATE | SUPPORT_MAP | SUPPORT_LOCATE ATTR_CLEAN_START = 'clean_start' @@ -222,3 +222,7 @@ class NeatoConnectedVacuum(StateVacuumDevice): def locate(self, **kwargs): """Locate the robot by making it emit a sound.""" self.robot.locate() + + def clean_spot(self, **kwargs): + """Run a spot cleaning starting from the base.""" + self.robot.start_spot_cleaning() From ea8bb28d21e28fe2b07e13adfb642c04854bd9d2 Mon Sep 17 00:00:00 2001 From: Vincent KHERBACHE Date: Tue, 8 Jan 2019 00:13:09 +0100 Subject: [PATCH 042/235] =?UTF-8?q?Fix=20french=20Amazon=20Polly=20voice?= =?UTF-8?q?=20'L=C3=A9a'.=20(#19852)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The accent must be removed (Léa -> Lea) just like the other voices (eg. Celine, Peneloppe) to match with Amazon voices ID. Fun fact: there is no alternative name for "Léa" on Amazon Polly documentation: https://docs.aws.amazon.com/en_us/polly/latest/dg/voicelist.html, probably just omitted. Mitigation: alternative voices (with and without accents) can be put into `SUPPORTED_VOICES`, both `voice.get('Id')` and `voice.get('Name')` must be then checked for a match. This fixes #19802. --- homeassistant/components/tts/amazon_polly.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/tts/amazon_polly.py b/homeassistant/components/tts/amazon_polly.py index 1dfb741bb42..0102a1fec09 100644 --- a/homeassistant/components/tts/amazon_polly.py +++ b/homeassistant/components/tts/amazon_polly.py @@ -43,7 +43,7 @@ SUPPORTED_VOICES = [ 'Joey', 'Justin', 'Matthew', 'Ivy', 'Joanna', 'Kendra', 'Kimberly', 'Salli', # English 'Geraint', # English Welsh - 'Mathieu', 'Celine', 'Léa', # French + 'Mathieu', 'Celine', 'Lea', # French 'Chantal', # French Canadian 'Hans', 'Marlene', 'Vicki', # German 'Aditi', # Hindi From fb9aad87912f5d1167950b17191bdce1b12466b3 Mon Sep 17 00:00:00 2001 From: emontnemery Date: Tue, 8 Jan 2019 07:21:26 +0100 Subject: [PATCH 043/235] Small cleanup of MQTT light (#19816) * Small refactor of MQTT light removing unused variable --- .../components/light/mqtt/schema_basic.py | 45 +++++++++---------- 1 file changed, 22 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/light/mqtt/schema_basic.py b/homeassistant/components/light/mqtt/schema_basic.py index a263ed66d6d..351eb4c464c 100644 --- a/homeassistant/components/light/mqtt/schema_basic.py +++ b/homeassistant/components/light/mqtt/schema_basic.py @@ -134,7 +134,6 @@ class MqttLight(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, self._color_temp = None self._effect = None self._white_value = None - self._supported_features = 0 self._topic = None self._payload = None @@ -241,27 +240,6 @@ class MqttLight(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, self._optimistic_xy = \ optimistic or topic[CONF_XY_STATE_TOPIC] is None - self._supported_features = 0 - self._supported_features |= ( - topic[CONF_RGB_COMMAND_TOPIC] is not None and - (SUPPORT_COLOR | SUPPORT_BRIGHTNESS)) - self._supported_features |= ( - topic[CONF_BRIGHTNESS_COMMAND_TOPIC] is not None and - SUPPORT_BRIGHTNESS) - self._supported_features |= ( - topic[CONF_COLOR_TEMP_COMMAND_TOPIC] is not None and - SUPPORT_COLOR_TEMP) - self._supported_features |= ( - topic[CONF_EFFECT_COMMAND_TOPIC] is not None and - SUPPORT_EFFECT) - self._supported_features |= ( - topic[CONF_HS_COMMAND_TOPIC] is not None and SUPPORT_COLOR) - self._supported_features |= ( - topic[CONF_WHITE_VALUE_COMMAND_TOPIC] is not None and - SUPPORT_WHITE_VALUE) - self._supported_features |= ( - topic[CONF_XY_COMMAND_TOPIC] is not None and SUPPORT_COLOR) - async def _subscribe_topics(self): """(Re)Subscribe to topics.""" topics = {} @@ -561,7 +539,28 @@ class MqttLight(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, @property def supported_features(self): """Flag supported features.""" - return self._supported_features + supported_features = 0 + supported_features |= ( + self._topic[CONF_RGB_COMMAND_TOPIC] is not None and + (SUPPORT_COLOR | SUPPORT_BRIGHTNESS)) + supported_features |= ( + self._topic[CONF_BRIGHTNESS_COMMAND_TOPIC] is not None and + SUPPORT_BRIGHTNESS) + supported_features |= ( + self._topic[CONF_COLOR_TEMP_COMMAND_TOPIC] is not None and + SUPPORT_COLOR_TEMP) + supported_features |= ( + self._topic[CONF_EFFECT_COMMAND_TOPIC] is not None and + SUPPORT_EFFECT) + supported_features |= ( + self._topic[CONF_HS_COMMAND_TOPIC] is not None and SUPPORT_COLOR) + supported_features |= ( + self._topic[CONF_WHITE_VALUE_COMMAND_TOPIC] is not None and + SUPPORT_WHITE_VALUE) + supported_features |= ( + self._topic[CONF_XY_COMMAND_TOPIC] is not None and SUPPORT_COLOR) + + return supported_features async def async_turn_on(self, **kwargs): """Turn the device on. From c7a32e59b78a7999814400567d68ba37f21b3bae Mon Sep 17 00:00:00 2001 From: kennedyshead Date: Tue, 8 Jan 2019 09:54:22 +0100 Subject: [PATCH 044/235] Fix state and attribute fetching in vasttrafik (#19856) * Fixing state and attribute fetching * Fixing state and attribute fetching * Setting None state * Need to brreak loop --- homeassistant/components/sensor/vasttrafik.py | 64 +++++++++---------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/sensor/vasttrafik.py b/homeassistant/components/sensor/vasttrafik.py index afa07f46fde..124b0ff44ea 100644 --- a/homeassistant/components/sensor/vasttrafik.py +++ b/homeassistant/components/sensor/vasttrafik.py @@ -84,6 +84,8 @@ class VasttrafikDepartureSensor(Entity): self._lines = lines if lines else None self._delay = timedelta(minutes=delay) self._departureboard = None + self._state = None + self._attributes = None @property def name(self): @@ -98,42 +100,12 @@ class VasttrafikDepartureSensor(Entity): @property def device_state_attributes(self): """Return the state attributes.""" - if not self._departureboard: - return - - for departure in self._departureboard: - line = departure.get('sname') - if not self._lines or line in self._lines: - params = { - ATTR_ACCESSIBILITY: departure.get('accessibility'), - ATTR_ATTRIBUTION: CONF_ATTRIBUTION, - ATTR_DIRECTION: departure.get('direction'), - ATTR_LINE: departure.get('sname'), - ATTR_TRACK: departure.get('track'), - } - return {k: v for k, v in params.items() if v} + return self._attributes @property def state(self): """Return the next departure time.""" - if not self._departureboard: - _LOGGER.debug( - "No departures from %s heading %s", - self._departure['name'], - self._heading['name'] if self._heading else 'ANY') - return - for departure in self._departureboard: - line = departure.get('sname') - if not self._lines or line in self._lines: - if 'rtTime' in self._departureboard[0]: - return self._departureboard[0]['rtTime'] - return self._departureboard[0]['time'] - # No departures of given lines found - _LOGGER.debug( - "No departures from %s heading %s on line(s) %s", - self._departure['name'], - self._heading['name'] if self._heading else 'ANY', - ', '.join((str(line) for line in self._lines))) + return self._state @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): @@ -146,3 +118,31 @@ class VasttrafikDepartureSensor(Entity): except self._vasttrafik.Error: _LOGGER.debug("Unable to read departure board, updating token") self._planner.update_token() + + if not self._departureboard: + _LOGGER.debug( + "No departures from %s heading %s", + self._departure['name'], + self._heading['name'] if self._heading else 'ANY') + self._state = None + self._attributes = {} + else: + for departure in self._departureboard: + line = departure.get('sname') + if not self._lines or line in self._lines: + if 'rtTime' in self._departureboard[0]: + self._state = self._departureboard[0]['rtTime'] + else: + self._state = self._departureboard[0]['time'] + + params = { + ATTR_ACCESSIBILITY: departure.get('accessibility'), + ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + ATTR_DIRECTION: departure.get('direction'), + ATTR_LINE: departure.get('sname'), + ATTR_TRACK: departure.get('track'), + } + + self._attributes = { + k: v for k, v in params.items() if v} + break From 4bbfc04f5eb1c12a0631a00ba7a469508f24e957 Mon Sep 17 00:00:00 2001 From: Malte Franken Date: Tue, 8 Jan 2019 21:24:57 +1100 Subject: [PATCH 045/235] Geo Location -> Geolocation (comments and default group name) (#19865) * fixed geolocation naming in comments * fixed geolocation naming in default group name * fixed link to documentation (after https://github.com/home-assistant/home-assistant.io/pull/8086) --- homeassistant/components/automation/geo_location.py | 4 ++-- homeassistant/components/geo_location/__init__.py | 8 ++++---- homeassistant/components/geo_location/demo.py | 12 ++++++------ 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/automation/geo_location.py b/homeassistant/components/automation/geo_location.py index 537646fefc1..33ef00da380 100644 --- a/homeassistant/components/automation/geo_location.py +++ b/homeassistant/components/automation/geo_location.py @@ -1,9 +1,9 @@ """ -Offer geo location automation rules. +Offer geolocation automation rules. For more details about this automation trigger, please refer to the documentation at -https://home-assistant.io/docs/automation/trigger/#geo-location-trigger +https://home-assistant.io/docs/automation/trigger/#geolocation-trigger """ import voluptuous as vol diff --git a/homeassistant/components/geo_location/__init__.py b/homeassistant/components/geo_location/__init__.py index 495c9e1744b..73a97d4666d 100644 --- a/homeassistant/components/geo_location/__init__.py +++ b/homeassistant/components/geo_location/__init__.py @@ -1,5 +1,5 @@ """ -Geo Location component. +Geolocation component. For more details about this component, please refer to the documentation at https://home-assistant.io/components/geo_location/ @@ -22,13 +22,13 @@ DOMAIN = 'geo_location' ENTITY_ID_FORMAT = DOMAIN + '.{}' -GROUP_NAME_ALL_EVENTS = 'All Geo Location Events' +GROUP_NAME_ALL_EVENTS = 'All Geolocation Events' SCAN_INTERVAL = timedelta(seconds=60) async def async_setup(hass, config): - """Set up the Geo Location component.""" + """Set up the Geolocation component.""" component = EntityComponent( _LOGGER, DOMAIN, hass, SCAN_INTERVAL, GROUP_NAME_ALL_EVENTS) await component.async_setup(config) @@ -36,7 +36,7 @@ async def async_setup(hass, config): class GeoLocationEvent(Entity): - """This represents an external event with an associated geo location.""" + """This represents an external event with an associated geolocation.""" @property def state(self): diff --git a/homeassistant/components/geo_location/demo.py b/homeassistant/components/geo_location/demo.py index 6c5cc2fe147..995cbeefa36 100644 --- a/homeassistant/components/geo_location/demo.py +++ b/homeassistant/components/geo_location/demo.py @@ -1,5 +1,5 @@ """ -Demo platform for the geo location component. +Demo platform for the geolocation component. For more details about this platform, please refer to the documentation https://home-assistant.io/components/demo/ @@ -30,15 +30,15 @@ SOURCE = 'demo' def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Demo geo locations.""" + """Set up the Demo geolocations.""" DemoManager(hass, add_entities) class DemoManager: - """Device manager for demo geo location events.""" + """Device manager for demo geolocation events.""" def __init__(self, hass, add_entities): - """Initialise the demo geo location event manager.""" + """Initialise the demo geolocation event manager.""" self._hass = hass self._add_entities = add_entities self._managed_devices = [] @@ -91,7 +91,7 @@ class DemoManager: class DemoGeoLocationEvent(GeoLocationEvent): - """This represents a demo geo location event.""" + """This represents a demo geolocation event.""" def __init__(self, name, distance, latitude, longitude, unit_of_measurement): @@ -114,7 +114,7 @@ class DemoGeoLocationEvent(GeoLocationEvent): @property def should_poll(self): - """No polling needed for a demo geo location event.""" + """No polling needed for a demo geolocation event.""" return False @property From 410f19c77718f1d6c5a1c968c1197421436b6994 Mon Sep 17 00:00:00 2001 From: Rene Nulsch <33263735+ReneNulschDE@users.noreply.github.com> Date: Tue, 8 Jan 2019 14:06:08 +0100 Subject: [PATCH 046/235] Replace MyChevy persistant_notification with error log entry (#19804) The mychevy service is notoriously unreliable, often only having 50% uptime. Previously a persistent notification was emitted when the platform errored out. Users have found that is happening too often, so instead log an error when this happens instead. --- homeassistant/components/sensor/mychevy.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/sensor/mychevy.py b/homeassistant/components/sensor/mychevy.py index 989126acc20..a835321991d 100644 --- a/homeassistant/components/sensor/mychevy.py +++ b/homeassistant/components/sensor/mychevy.py @@ -8,7 +8,7 @@ import logging from homeassistant.components.mychevy import ( EVSensorConfig, DOMAIN as MYCHEVY_DOMAIN, MYCHEVY_ERROR, MYCHEVY_SUCCESS, - NOTIFICATION_ID, NOTIFICATION_TITLE, UPDATE_TOPIC, ERROR_TOPIC + UPDATE_TOPIC, ERROR_TOPIC ) from homeassistant.components.sensor import ENTITY_ID_FORMAT from homeassistant.core import callback @@ -74,13 +74,10 @@ class MyChevyStatus(Entity): @callback def error(self): """Update state, trigger updates.""" - if self._state != MYCHEVY_ERROR: - self.hass.components.persistent_notification.create( - "Error:
Connection to mychevy website failed. " - "This probably means the mychevy to OnStar link is down.", - title=NOTIFICATION_TITLE, - notification_id=NOTIFICATION_ID) - self._state = MYCHEVY_ERROR + _LOGGER.error( + "Connection to mychevy website failed. " + "This probably means the mychevy to OnStar link is down") + self._state = MYCHEVY_ERROR self.async_schedule_update_ha_state() @property From 377b129c9c67178c241326e490e6dd56042255db Mon Sep 17 00:00:00 2001 From: kennedyshead Date: Tue, 8 Jan 2019 15:14:16 +0100 Subject: [PATCH 047/235] Make asuswrt sensor optional (#19736) @kennedyshead * Dont load if not in config * Adding config options for sensors * Fixed mistake with iterating over wrong things * lint * lint * Setting None state * Using .get when fetching optional config --- homeassistant/components/asuswrt.py | 9 +++++++-- homeassistant/components/sensor/asuswrt.py | 22 ++++++++++++++++------ 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/asuswrt.py b/homeassistant/components/asuswrt.py index 25e7a59f47c..8dedb1f640a 100644 --- a/homeassistant/components/asuswrt.py +++ b/homeassistant/components/asuswrt.py @@ -26,6 +26,8 @@ CONF_SSH_KEY = 'ssh_key' CONF_REQUIRE_IP = 'require_ip' DEFAULT_SSH_PORT = 22 SECRET_GROUP = 'Password or SSH Key' +CONF_SENSORS = 'sensors' +SENSOR_TYPES = ['upload_speed', 'download_speed', 'download', 'upload'] CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ @@ -37,7 +39,9 @@ CONFIG_SCHEMA = vol.Schema({ vol.Optional(CONF_REQUIRE_IP, default=True): cv.boolean, vol.Exclusive(CONF_PASSWORD, SECRET_GROUP): cv.string, vol.Exclusive(CONF_SSH_KEY, SECRET_GROUP): cv.isfile, - vol.Exclusive(CONF_PUB_KEY, SECRET_GROUP): cv.isfile + vol.Exclusive(CONF_PUB_KEY, SECRET_GROUP): cv.isfile, + vol.Optional(CONF_SENSORS): vol.All( + cv.ensure_list, [vol.In(SENSOR_TYPES)]), }), }, extra=vol.ALLOW_EXTRA) @@ -62,7 +66,8 @@ async def async_setup(hass, config): hass.data[DATA_ASUSWRT] = api hass.async_create_task(async_load_platform( - hass, 'sensor', DOMAIN, {}, config)) + hass, 'sensor', DOMAIN, config[DOMAIN].get(CONF_SENSORS), config)) hass.async_create_task(async_load_platform( hass, 'device_tracker', DOMAIN, {}, config)) + return True diff --git a/homeassistant/components/sensor/asuswrt.py b/homeassistant/components/sensor/asuswrt.py index 876f0dfd559..6af59ec1809 100644 --- a/homeassistant/components/sensor/asuswrt.py +++ b/homeassistant/components/sensor/asuswrt.py @@ -17,13 +17,23 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_platform( hass, config, add_entities, discovery_info=None): """Set up the asuswrt sensors.""" + if discovery_info is None: + return + api = hass.data[DATA_ASUSWRT] - add_entities([ - AsuswrtRXSensor(api), - AsuswrtTXSensor(api), - AsuswrtTotalRXSensor(api), - AsuswrtTotalTXSensor(api) - ]) + + devices = [] + + if 'download' in discovery_info: + devices.append(AsuswrtTotalRXSensor(api)) + if 'upload' in discovery_info: + devices.append(AsuswrtTotalTXSensor(api)) + if 'download_speed' in discovery_info: + devices.append(AsuswrtRXSensor(api)) + if 'upload_speed' in discovery_info: + devices.append(AsuswrtTXSensor(api)) + + add_entities(devices) class AsuswrtSensor(Entity): From 406b45c6e7bea7413d2f33831a5e36c0fc565e59 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Tue, 8 Jan 2019 15:22:45 +0100 Subject: [PATCH 048/235] Upgrade bcrypt to 3.1.5 (#19854) --- 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 b06287bcf17..155a0ec6725 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -2,7 +2,7 @@ aiohttp==3.5.1 astral==1.7.1 async_timeout==3.0.1 attrs==18.2.0 -bcrypt==3.1.4 +bcrypt==3.1.5 certifi>=2018.04.16 jinja2>=2.10 PyJWT==1.6.4 diff --git a/requirements_all.txt b/requirements_all.txt index 9dbd4275389..7172a7027ef 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -3,7 +3,7 @@ aiohttp==3.5.1 astral==1.7.1 async_timeout==3.0.1 attrs==18.2.0 -bcrypt==3.1.4 +bcrypt==3.1.5 certifi>=2018.04.16 jinja2>=2.10 PyJWT==1.6.4 diff --git a/setup.py b/setup.py index 0581d7bfcfa..4457f725585 100755 --- a/setup.py +++ b/setup.py @@ -36,7 +36,7 @@ REQUIRES = [ 'astral==1.7.1', 'async_timeout==3.0.1', 'attrs==18.2.0', - 'bcrypt==3.1.4', + 'bcrypt==3.1.5', 'certifi>=2018.04.16', 'jinja2>=2.10', 'PyJWT==1.6.4', From bb37cf906c998573cf50af512e52c6d736d6ecc5 Mon Sep 17 00:00:00 2001 From: emontnemery Date: Tue, 8 Jan 2019 16:45:38 +0100 Subject: [PATCH 049/235] Cleanup if discovered mqtt lock can't be added (#19746) * Cleanup if discovered mqtt lock can't be added --- homeassistant/components/lock/mqtt.py | 15 +++++++++---- tests/components/lock/test_mqtt.py | 32 +++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/lock/mqtt.py b/homeassistant/components/lock/mqtt.py index ad5ba34dbb0..5574c7e4e59 100644 --- a/homeassistant/components/lock/mqtt.py +++ b/homeassistant/components/lock/mqtt.py @@ -18,7 +18,8 @@ from homeassistant.components.mqtt import ( from homeassistant.const import ( CONF_DEVICE, CONF_NAME, CONF_OPTIMISTIC, CONF_VALUE_TEMPLATE) from homeassistant.components import mqtt, lock -from homeassistant.components.mqtt.discovery import MQTT_DISCOVERY_NEW +from homeassistant.components.mqtt.discovery import ( + MQTT_DISCOVERY_NEW, clear_discovery_hash) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.typing import HomeAssistantType, ConfigType @@ -57,9 +58,15 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Set up MQTT lock dynamically through MQTT discovery.""" async def async_discover(discovery_payload): """Discover and add an MQTT lock.""" - config = PLATFORM_SCHEMA(discovery_payload) - await _async_setup_entity(hass, config, async_add_entities, - discovery_payload[ATTR_DISCOVERY_HASH]) + try: + discovery_hash = discovery_payload[ATTR_DISCOVERY_HASH] + config = PLATFORM_SCHEMA(discovery_payload) + await _async_setup_entity(hass, config, async_add_entities, + discovery_hash) + except Exception: + if discovery_hash: + clear_discovery_hash(hass, discovery_hash) + raise async_dispatcher_connect( hass, MQTT_DISCOVERY_NEW.format(lock.DOMAIN, 'mqtt'), diff --git a/tests/components/lock/test_mqtt.py b/tests/components/lock/test_mqtt.py index 0fcb97f1b68..f3b7c45d38a 100644 --- a/tests/components/lock/test_mqtt.py +++ b/tests/components/lock/test_mqtt.py @@ -182,6 +182,38 @@ async def test_discovery_removal_lock(hass, mqtt_mock, caplog): assert state is None +async def test_discovery_broken(hass, mqtt_mock, caplog): + """Test handling of bad discovery message.""" + entry = MockConfigEntry(domain=mqtt.DOMAIN) + await async_start(hass, 'homeassistant', {}, entry) + + data1 = ( + '{ "name": "Beer" }' + ) + data2 = ( + '{ "name": "Milk",' + ' "command_topic": "test_topic" }' + ) + + async_fire_mqtt_message(hass, 'homeassistant/lock/bla/config', + data1) + await hass.async_block_till_done() + + state = hass.states.get('lock.beer') + assert state is None + + async_fire_mqtt_message(hass, 'homeassistant/lock/bla/config', + data2) + await hass.async_block_till_done() + await hass.async_block_till_done() + + state = hass.states.get('lock.milk') + assert state is not None + assert state.name == 'Milk' + state = hass.states.get('lock.beer') + assert state is None + + async def test_entity_device_info_with_identifier(hass, mqtt_mock): """Test MQTT lock device registry integration.""" entry = MockConfigEntry(domain=mqtt.DOMAIN) From 1a5fe3d8809c395fbef8201199cefb6c52c9e00a Mon Sep 17 00:00:00 2001 From: emontnemery Date: Tue, 8 Jan 2019 16:48:42 +0100 Subject: [PATCH 050/235] Cleanup if discovered mqtt cover can't be added (#19743) * Cleanup if discovered mqtt cover can't be added --- homeassistant/components/cover/mqtt.py | 15 ++++++++--- tests/components/cover/test_mqtt.py | 35 +++++++++++++++++++++++++- 2 files changed, 45 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/cover/mqtt.py b/homeassistant/components/cover/mqtt.py index 3926c84cb92..f5001c9c495 100644 --- a/homeassistant/components/cover/mqtt.py +++ b/homeassistant/components/cover/mqtt.py @@ -24,7 +24,8 @@ from homeassistant.components.mqtt import ( CONF_COMMAND_TOPIC, CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, CONF_RETAIN, valid_publish_topic, valid_subscribe_topic, MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, subscription) -from homeassistant.components.mqtt.discovery import MQTT_DISCOVERY_NEW +from homeassistant.components.mqtt.discovery import ( + MQTT_DISCOVERY_NEW, clear_discovery_hash) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.typing import HomeAssistantType, ConfigType @@ -136,9 +137,15 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Set up MQTT cover dynamically through MQTT discovery.""" async def async_discover(discovery_payload): """Discover and add an MQTT cover.""" - config = PLATFORM_SCHEMA(discovery_payload) - await _async_setup_entity(config, async_add_entities, - discovery_payload[ATTR_DISCOVERY_HASH]) + try: + discovery_hash = discovery_payload[ATTR_DISCOVERY_HASH] + config = PLATFORM_SCHEMA(discovery_payload) + await _async_setup_entity(config, async_add_entities, + discovery_hash) + except Exception: + if discovery_hash: + clear_discovery_hash(hass, discovery_hash) + raise async_dispatcher_connect( hass, MQTT_DISCOVERY_NEW.format(cover.DOMAIN, 'mqtt'), diff --git a/tests/components/cover/test_mqtt.py b/tests/components/cover/test_mqtt.py index a31f393c507..1616a93d0b5 100644 --- a/tests/components/cover/test_mqtt.py +++ b/tests/components/cover/test_mqtt.py @@ -1067,7 +1067,7 @@ async def test_discovery_removal_cover(hass, mqtt_mock, caplog): async def test_discovery_update_cover(hass, mqtt_mock, caplog): - """Test removal of discovered cover.""" + """Test update of discovered cover.""" entry = MockConfigEntry(domain=mqtt.DOMAIN) await async_start(hass, 'homeassistant', {}, entry) data1 = ( @@ -1098,6 +1098,39 @@ async def test_discovery_update_cover(hass, mqtt_mock, caplog): assert state is None +async def test_discovery_broken(hass, mqtt_mock, caplog): + """Test handling of bad discovery message.""" + entry = MockConfigEntry(domain=mqtt.DOMAIN) + await async_start(hass, 'homeassistant', {}, entry) + + data1 = ( + '{ "name": "Beer",' + ' "command_topic": "test_topic#" }' + ) + data2 = ( + '{ "name": "Milk",' + ' "command_topic": "test_topic" }' + ) + + async_fire_mqtt_message(hass, 'homeassistant/cover/bla/config', + data1) + await hass.async_block_till_done() + + state = hass.states.get('cover.beer') + assert state is None + + async_fire_mqtt_message(hass, 'homeassistant/cover/bla/config', + data2) + await hass.async_block_till_done() + await hass.async_block_till_done() + + state = hass.states.get('cover.milk') + assert state is not None + assert state.name == 'Milk' + state = hass.states.get('cover.beer') + assert state is None + + async def test_unique_id(hass): """Test unique_id option only creates one cover per id.""" await async_mock_mqtt_component(hass) From 44f615154833824f817d056fe1f97734557f0d11 Mon Sep 17 00:00:00 2001 From: emontnemery Date: Tue, 8 Jan 2019 16:49:47 +0100 Subject: [PATCH 051/235] Cleanup if discovered mqtt alarm can't be added (#19742) * Cleanup if discovered mqtt alarm can't be added --- .../components/alarm_control_panel/mqtt.py | 15 ++++++-- .../alarm_control_panel/test_mqtt.py | 37 ++++++++++++++++++- 2 files changed, 47 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/alarm_control_panel/mqtt.py b/homeassistant/components/alarm_control_panel/mqtt.py index c943a513c45..0113cfa93b1 100644 --- a/homeassistant/components/alarm_control_panel/mqtt.py +++ b/homeassistant/components/alarm_control_panel/mqtt.py @@ -21,7 +21,8 @@ from homeassistant.components.mqtt import ( CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, subscription) -from homeassistant.components.mqtt.discovery import MQTT_DISCOVERY_NEW +from homeassistant.components.mqtt.discovery import ( + MQTT_DISCOVERY_NEW, clear_discovery_hash) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.typing import HomeAssistantType, ConfigType @@ -62,9 +63,15 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Set up MQTT alarm control panel dynamically through MQTT discovery.""" async def async_discover(discovery_payload): """Discover and add an MQTT alarm control panel.""" - config = PLATFORM_SCHEMA(discovery_payload) - await _async_setup_entity(config, async_add_entities, - discovery_payload[ATTR_DISCOVERY_HASH]) + try: + discovery_hash = discovery_payload[ATTR_DISCOVERY_HASH] + config = PLATFORM_SCHEMA(discovery_payload) + await _async_setup_entity(config, async_add_entities, + discovery_hash) + except Exception: + if discovery_hash: + clear_discovery_hash(hass, discovery_hash) + raise async_dispatcher_connect( hass, MQTT_DISCOVERY_NEW.format(alarm.DOMAIN, 'mqtt'), diff --git a/tests/components/alarm_control_panel/test_mqtt.py b/tests/components/alarm_control_panel/test_mqtt.py index 9f161aaf083..1a89e2382e3 100644 --- a/tests/components/alarm_control_panel/test_mqtt.py +++ b/tests/components/alarm_control_panel/test_mqtt.py @@ -300,7 +300,7 @@ async def test_discovery_removal_alarm(hass, mqtt_mock, caplog): async def test_discovery_update_alarm(hass, mqtt_mock, caplog): - """Test removal of discovered alarm_control_panel.""" + """Test update of discovered alarm_control_panel.""" entry = MockConfigEntry(domain=mqtt.DOMAIN) await async_start(hass, 'homeassistant', {}, entry) @@ -338,6 +338,41 @@ async def test_discovery_update_alarm(hass, mqtt_mock, caplog): assert state is None +async def test_discovery_broken(hass, mqtt_mock, caplog): + """Test handling of bad discovery message.""" + entry = MockConfigEntry(domain=mqtt.DOMAIN) + await async_start(hass, 'homeassistant', {}, entry) + + data1 = ( + '{ "name": "Beer" }' + ) + data2 = ( + '{ "name": "Milk",' + ' "status_topic": "test_topic",' + ' "command_topic": "test_topic" }' + ) + + async_fire_mqtt_message(hass, + 'homeassistant/alarm_control_panel/bla/config', + data1) + await hass.async_block_till_done() + + state = hass.states.get('alarm_control_panel.beer') + assert state is None + + async_fire_mqtt_message(hass, + 'homeassistant/alarm_control_panel/bla/config', + data2) + await hass.async_block_till_done() + await hass.async_block_till_done() + + state = hass.states.get('alarm_control_panel.milk') + assert state is not None + assert state.name == 'Milk' + state = hass.states.get('alarm_control_panel.beer') + assert state is None + + async def test_entity_device_info_with_identifier(hass, mqtt_mock): """Test MQTT alarm control panel device registry integration.""" entry = MockConfigEntry(domain=mqtt.DOMAIN) From 203701bc7c1069017759c6ff21107a1e96b08e95 Mon Sep 17 00:00:00 2001 From: emontnemery Date: Tue, 8 Jan 2019 16:51:03 +0100 Subject: [PATCH 052/235] Cleanup if discovered mqtt fan can't be added (#19741) * Cleanup if discovered mqtt fan can't be added --- homeassistant/components/fan/mqtt.py | 19 ++++++++----- tests/components/fan/test_mqtt.py | 40 +++++++++++++++++++++++++--- 2 files changed, 49 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/fan/mqtt.py b/homeassistant/components/fan/mqtt.py index 932a4584e2f..3adb821eba5 100644 --- a/homeassistant/components/fan/mqtt.py +++ b/homeassistant/components/fan/mqtt.py @@ -25,7 +25,8 @@ from homeassistant.components.fan import (SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH, FanEntity, SUPPORT_SET_SPEED, SUPPORT_OSCILLATE, SPEED_OFF, ATTR_SPEED) -from homeassistant.components.mqtt.discovery import MQTT_DISCOVERY_NEW +from homeassistant.components.mqtt.discovery import ( + MQTT_DISCOVERY_NEW, clear_discovery_hash) _LOGGER = logging.getLogger(__name__) @@ -86,23 +87,29 @@ PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({ async def async_setup_platform(hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None): """Set up MQTT fan through configuration.yaml.""" - await _async_setup_entity(hass, config, async_add_entities) + await _async_setup_entity(config, async_add_entities) async def async_setup_entry(hass, config_entry, async_add_entities): """Set up MQTT fan dynamically through MQTT discovery.""" async def async_discover(discovery_payload): """Discover and add a MQTT fan.""" - config = PLATFORM_SCHEMA(discovery_payload) - await _async_setup_entity(hass, config, async_add_entities, - discovery_payload[ATTR_DISCOVERY_HASH]) + try: + discovery_hash = discovery_payload[ATTR_DISCOVERY_HASH] + config = PLATFORM_SCHEMA(discovery_payload) + await _async_setup_entity(config, async_add_entities, + discovery_hash) + except Exception: + if discovery_hash: + clear_discovery_hash(hass, discovery_hash) + raise async_dispatcher_connect( hass, MQTT_DISCOVERY_NEW.format(fan.DOMAIN, 'mqtt'), async_discover) -async def _async_setup_entity(hass, config, async_add_entities, +async def _async_setup_entity(config, async_add_entities, discovery_hash=None): """Set up the MQTT fan.""" async_add_entities([MqttFan( diff --git a/tests/components/fan/test_mqtt.py b/tests/components/fan/test_mqtt.py index 405c20196a0..fea6f6dda74 100644 --- a/tests/components/fan/test_mqtt.py +++ b/tests/components/fan/test_mqtt.py @@ -3,7 +3,7 @@ import json from unittest.mock import ANY from homeassistant.setup import async_setup_component -from homeassistant.components import fan +from homeassistant.components import fan, mqtt from homeassistant.components.mqtt.discovery import async_start from homeassistant.const import ATTR_ASSUMED_STATE, STATE_UNAVAILABLE @@ -111,7 +111,7 @@ async def test_custom_availability_payload(hass, mqtt_mock): async def test_discovery_removal_fan(hass, mqtt_mock, caplog): """Test removal of discovered fan.""" - entry = MockConfigEntry(domain='mqtt') + entry = MockConfigEntry(domain=mqtt.DOMAIN) await async_start(hass, 'homeassistant', {}, entry) data = ( '{ "name": "Beer",' @@ -132,8 +132,8 @@ async def test_discovery_removal_fan(hass, mqtt_mock, caplog): async def test_discovery_update_fan(hass, mqtt_mock, caplog): - """Test removal of discovered fan.""" - entry = MockConfigEntry(domain='mqtt') + """Test update of discovered fan.""" + entry = MockConfigEntry(domain=mqtt.DOMAIN) await async_start(hass, 'homeassistant', {}, entry) data1 = ( '{ "name": "Beer",' @@ -163,6 +163,38 @@ async def test_discovery_update_fan(hass, mqtt_mock, caplog): assert state is None +async def test_discovery_broken(hass, mqtt_mock, caplog): + """Test handling of bad discovery message.""" + entry = MockConfigEntry(domain=mqtt.DOMAIN) + await async_start(hass, 'homeassistant', {}, entry) + + data1 = ( + '{ "name": "Beer" }' + ) + data2 = ( + '{ "name": "Milk",' + ' "command_topic": "test_topic" }' + ) + + async_fire_mqtt_message(hass, 'homeassistant/fan/bla/config', + data1) + await hass.async_block_till_done() + + state = hass.states.get('fan.beer') + assert state is None + + async_fire_mqtt_message(hass, 'homeassistant/fan/bla/config', + data2) + await hass.async_block_till_done() + await hass.async_block_till_done() + + state = hass.states.get('fan.milk') + assert state is not None + assert state.name == 'Milk' + state = hass.states.get('fan.beer') + assert state is None + + async def test_unique_id(hass): """Test unique_id option only creates one fan per id.""" await async_mock_mqtt_component(hass) From 0cea54cea1b1c3d971913128c94c8b2668b89a84 Mon Sep 17 00:00:00 2001 From: emontnemery Date: Tue, 8 Jan 2019 16:53:02 +0100 Subject: [PATCH 053/235] Cleanup if discovered mqtt climate can't be added (#19739) * Cleanup if discovered mqtt climate can't be added --- homeassistant/components/climate/mqtt.py | 15 +++++++--- tests/components/climate/test_mqtt.py | 35 +++++++++++++++++++++++- 2 files changed, 45 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/climate/mqtt.py b/homeassistant/components/climate/mqtt.py index 96de0709dc8..3219b71c1bd 100644 --- a/homeassistant/components/climate/mqtt.py +++ b/homeassistant/components/climate/mqtt.py @@ -25,7 +25,8 @@ from homeassistant.components.mqtt import ( CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, MQTT_BASE_PLATFORM_SCHEMA, MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, subscription) -from homeassistant.components.mqtt.discovery import MQTT_DISCOVERY_NEW +from homeassistant.components.mqtt.discovery import ( + MQTT_DISCOVERY_NEW, clear_discovery_hash) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.typing import HomeAssistantType, ConfigType @@ -158,9 +159,15 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Set up MQTT climate device dynamically through MQTT discovery.""" async def async_discover(discovery_payload): """Discover and add a MQTT climate device.""" - config = PLATFORM_SCHEMA(discovery_payload) - await _async_setup_entity(hass, config, async_add_entities, - discovery_payload[ATTR_DISCOVERY_HASH]) + try: + discovery_hash = discovery_payload[ATTR_DISCOVERY_HASH] + config = PLATFORM_SCHEMA(discovery_payload) + await _async_setup_entity(hass, config, async_add_entities, + discovery_hash) + except Exception: + if discovery_hash: + clear_discovery_hash(hass, discovery_hash) + raise async_dispatcher_connect( hass, MQTT_DISCOVERY_NEW.format(climate.DOMAIN, 'mqtt'), diff --git a/tests/components/climate/test_mqtt.py b/tests/components/climate/test_mqtt.py index 1fd45701a95..a2aa424eeee 100644 --- a/tests/components/climate/test_mqtt.py +++ b/tests/components/climate/test_mqtt.py @@ -719,7 +719,7 @@ async def test_discovery_removal_climate(hass, mqtt_mock, caplog): async def test_discovery_update_climate(hass, mqtt_mock, caplog): - """Test removal of discovered climate.""" + """Test update of discovered climate.""" entry = MockConfigEntry(domain=mqtt.DOMAIN) await async_start(hass, 'homeassistant', {}, entry) data1 = ( @@ -749,6 +749,39 @@ async def test_discovery_update_climate(hass, mqtt_mock, caplog): assert state is None +async def test_discovery_broken(hass, mqtt_mock, caplog): + """Test handling of bad discovery message.""" + entry = MockConfigEntry(domain=mqtt.DOMAIN) + await async_start(hass, 'homeassistant', {}, entry) + + data1 = ( + '{ "name": "Beer",' + ' "power_command_topic": "test_topic#" }' + ) + data2 = ( + '{ "name": "Milk", ' + ' "power_command_topic": "test_topic" }' + ) + + async_fire_mqtt_message(hass, 'homeassistant/climate/bla/config', + data1) + await hass.async_block_till_done() + + state = hass.states.get('climate.beer') + assert state is None + + async_fire_mqtt_message(hass, 'homeassistant/climate/bla/config', + data2) + await hass.async_block_till_done() + await hass.async_block_till_done() + + state = hass.states.get('climate.milk') + assert state is not None + assert state.name == 'Milk' + state = hass.states.get('climate.beer') + assert state is None + + async def test_entity_device_info_with_identifier(hass, mqtt_mock): """Test MQTT climate device registry integration.""" entry = MockConfigEntry(domain=mqtt.DOMAIN) From acdf9c7ce2c0bf746fb9a8b04bfeffa42ec1ed20 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Tue, 8 Jan 2019 11:20:50 -0500 Subject: [PATCH 054/235] Relay events for onoff and levelcontrol output clusters in ZHA (#19863) * auto relay events for onoff and levelcontrol output clusters * fix docstring * correct copy/paste failure - review comment * add space - review comment --- homeassistant/components/zha/__init__.py | 18 ++++++++++---- homeassistant/components/zha/event.py | 30 ++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index 3fe8980c451..5dc8f628581 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -21,7 +21,7 @@ from homeassistant.helpers.entity_component import EntityComponent # Loading the config flow file will register the flow from . import config_flow # noqa # pylint: disable=unused-import from . import const as zha_const -from .event import ZhaEvent +from .event import ZhaEvent, ZhaRelayEvent from .const import ( COMPONENTS, CONF_BAUDRATE, CONF_DATABASE, CONF_DEVICE_CONFIG, CONF_RADIO_TYPE, CONF_USB_PATH, DATA_ZHA, DATA_ZHA_BRIDGE_ID, @@ -393,10 +393,18 @@ class ApplicationListener: if cluster.cluster_id in EVENTABLE_CLUSTERS: if cluster.endpoint.device.ieee not in self._events: self._events.update({cluster.endpoint.device.ieee: []}) - self._events[cluster.endpoint.device.ieee].append(ZhaEvent( - self._hass, - cluster - )) + from zigpy.zcl.clusters.general import OnOff, LevelControl + if discovery_attr == 'out_clusters' and \ + (cluster.cluster_id == OnOff.cluster_id or + cluster.cluster_id == LevelControl.cluster_id): + self._events[cluster.endpoint.device.ieee].append( + ZhaRelayEvent(self._hass, cluster) + ) + else: + self._events[cluster.endpoint.device.ieee].append(ZhaEvent( + self._hass, + cluster + )) if cluster.cluster_id in profile_clusters: return diff --git a/homeassistant/components/zha/event.py b/homeassistant/components/zha/event.py index 20175dd097f..7828a695a7b 100644 --- a/homeassistant/components/zha/event.py +++ b/homeassistant/components/zha/event.py @@ -67,3 +67,33 @@ class ZhaEvent(): }, EventOrigin.remote ) + + +class ZhaRelayEvent(ZhaEvent): + """Event relay that can be attached to zigbee clusters.""" + + @callback + def attribute_updated(self, attribute, value): + """Handle an attribute updated on this cluster.""" + self.zha_send_event( + self._cluster, + 'attribute_updated', + { + 'attribute_id': attribute, + 'attribute_name': self._cluster.attributes.get( + attribute, + ['Unknown'])[0], + 'value': value + } + ) + + @callback + def cluster_command(self, tsn, command_id, args): + """Handle a cluster command received on this cluster.""" + if self._cluster.server_commands is not None and\ + self._cluster.server_commands.get(command_id) is not None: + self.zha_send_event( + self._cluster, + self._cluster.server_commands.get(command_id)[0], + args + ) From c16453340471b5f016638adb1ef400a9871ee031 Mon Sep 17 00:00:00 2001 From: Lars Lydersen Date: Tue, 8 Jan 2019 20:36:57 +0100 Subject: [PATCH 055/235] Corrected formating. --- homeassistant/components/vacuum/neato.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/vacuum/neato.py b/homeassistant/components/vacuum/neato.py index 8abf2b2779a..e8f2cde45e7 100644 --- a/homeassistant/components/vacuum/neato.py +++ b/homeassistant/components/vacuum/neato.py @@ -24,7 +24,7 @@ DEPENDENCIES = ['neato'] SCAN_INTERVAL = timedelta(minutes=5) SUPPORT_NEATO = SUPPORT_BATTERY | SUPPORT_PAUSE | SUPPORT_RETURN_HOME | \ - SUPPORT_STOP | SUPPORT_START | SUPPORT_CLEAN_SPOT\ + SUPPORT_STOP | SUPPORT_START | SUPPORT_CLEAN_SPOT \ SUPPORT_STATE | SUPPORT_MAP | SUPPORT_LOCATE ATTR_CLEAN_START = 'clean_start' From 42821b5f64960f8be9269944b004085dcd822d62 Mon Sep 17 00:00:00 2001 From: Lars Lydersen Date: Tue, 8 Jan 2019 21:12:35 +0100 Subject: [PATCH 056/235] Added missing operator. --- homeassistant/components/vacuum/neato.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/vacuum/neato.py b/homeassistant/components/vacuum/neato.py index e8f2cde45e7..15ae3f068b6 100644 --- a/homeassistant/components/vacuum/neato.py +++ b/homeassistant/components/vacuum/neato.py @@ -24,7 +24,7 @@ DEPENDENCIES = ['neato'] SCAN_INTERVAL = timedelta(minutes=5) SUPPORT_NEATO = SUPPORT_BATTERY | SUPPORT_PAUSE | SUPPORT_RETURN_HOME | \ - SUPPORT_STOP | SUPPORT_START | SUPPORT_CLEAN_SPOT \ + SUPPORT_STOP | SUPPORT_START | SUPPORT_CLEAN_SPOT | \ SUPPORT_STATE | SUPPORT_MAP | SUPPORT_LOCATE ATTR_CLEAN_START = 'clean_start' From cc6e70a270d59b896904abfb8ef61282bd72b5c2 Mon Sep 17 00:00:00 2001 From: Steven Looman Date: Tue, 8 Jan 2019 22:05:36 +0100 Subject: [PATCH 057/235] Fix error when trying to log used UPnP device, if multiple found (#19875) --- homeassistant/components/upnp/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/upnp/__init__.py b/homeassistant/components/upnp/__init__.py index e360d4b18d3..d44cf2a8683 100644 --- a/homeassistant/components/upnp/__init__.py +++ b/homeassistant/components/upnp/__init__.py @@ -102,8 +102,10 @@ async def async_discover_and_construct(hass, udn=None) -> Device: # get the first/any discovery_info = discovery_infos[0] if len(discovery_infos) > 1: + device_name = discovery_info.get( + 'usn', discovery_info.get('ssdp_description', '')) _LOGGER.info('Detected multiple UPnP/IGD devices, using: %s', - discovery_info['igd_name']) + device_name) ssdp_description = discovery_info['ssdp_description'] return await Device.async_create_device(hass, ssdp_description) From c29bffc8d8138a28b94e643836cb8ddfb7a7a828 Mon Sep 17 00:00:00 2001 From: Pierre <3458055+BaQs@users.noreply.github.com> Date: Wed, 9 Jan 2019 00:31:39 +0100 Subject: [PATCH 058/235] Replace influxdb query by another query that is more lightweight (#19880) same as #6289 --- homeassistant/components/sensor/influxdb.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/influxdb.py b/homeassistant/components/sensor/influxdb.py index 0fc31ef273f..9f34580344c 100644 --- a/homeassistant/components/sensor/influxdb.py +++ b/homeassistant/components/sensor/influxdb.py @@ -111,7 +111,7 @@ class InfluxSensor(Entity): database=database, ssl=influx_conf['ssl'], verify_ssl=influx_conf['verify_ssl']) try: - influx.query("select * from /.*/ LIMIT 1;") + influx.query("SHOW DIAGNOSTICS;") self.connected = True self.data = InfluxSensorData( influx, query.get(CONF_GROUP_FUNCTION), query.get(CONF_FIELD), From f73bda1218f85d1860ab17f05f0bdc9d8f2ad440 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Lov=C3=A9n?= Date: Wed, 9 Jan 2019 05:08:20 +0100 Subject: [PATCH 059/235] Allow other icon prefixes than mdi: (#19872) --- homeassistant/helpers/config_validation.py | 4 ++-- tests/helpers/test_config_validation.py | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index c14f4e4fadb..245cc5d46bd 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -200,10 +200,10 @@ def icon(value): """Validate icon.""" value = str(value) - if value.startswith('mdi:'): + if ':' in value: return value - raise vol.Invalid('Icons should start with prefix "mdi:"') + raise vol.Invalid('Icons should be specifed on the form "prefix:name"') time_period_dict = vol.All( diff --git a/tests/helpers/test_config_validation.py b/tests/helpers/test_config_validation.py index 412882f0a01..791570981e2 100644 --- a/tests/helpers/test_config_validation.py +++ b/tests/helpers/test_config_validation.py @@ -258,11 +258,12 @@ def test_icon(): """Test icon validation.""" schema = vol.Schema(cv.icon) - for value in (False, 'work', 'icon:work'): + for value in (False, 'work'): with pytest.raises(vol.MultipleInvalid): schema(value) schema('mdi:work') + schema('custom:prefix') def test_time_period(): From 6d3343e4d1728708f68f711872dd153c75354045 Mon Sep 17 00:00:00 2001 From: Malte Franken Date: Wed, 9 Jan 2019 15:11:51 +1100 Subject: [PATCH 060/235] Geo Location -> Geolocation (class names and unit test comments) (#19877) * fixed geolocation naming in class names * fixed geolocation naming in comments in unit test --- homeassistant/components/geo_location/__init__.py | 2 +- homeassistant/components/geo_location/demo.py | 6 +++--- homeassistant/components/geo_location/geo_json_events.py | 4 ++-- .../geo_location/nsw_rural_fire_service_feed.py | 4 ++-- .../components/geo_location/usgs_earthquakes_feed.py | 4 ++-- tests/components/automation/test_geo_location.py | 2 +- tests/components/geo_location/test_demo.py | 2 +- tests/components/geo_location/test_init.py | 8 ++++---- 8 files changed, 16 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/geo_location/__init__.py b/homeassistant/components/geo_location/__init__.py index 73a97d4666d..4e05c5b41fe 100644 --- a/homeassistant/components/geo_location/__init__.py +++ b/homeassistant/components/geo_location/__init__.py @@ -35,7 +35,7 @@ async def async_setup(hass, config): return True -class GeoLocationEvent(Entity): +class GeolocationEvent(Entity): """This represents an external event with an associated geolocation.""" @property diff --git a/homeassistant/components/geo_location/demo.py b/homeassistant/components/geo_location/demo.py index 995cbeefa36..0e7274e7a0a 100644 --- a/homeassistant/components/geo_location/demo.py +++ b/homeassistant/components/geo_location/demo.py @@ -10,7 +10,7 @@ from math import cos, pi, radians, sin import random from typing import Optional -from homeassistant.components.geo_location import GeoLocationEvent +from homeassistant.components.geo_location import GeolocationEvent from homeassistant.helpers.event import track_time_interval _LOGGER = logging.getLogger(__name__) @@ -62,7 +62,7 @@ class DemoManager: cos(radians(home_latitude)) event_name = random.choice(EVENT_NAMES) - return DemoGeoLocationEvent(event_name, radius_in_km, latitude, + return DemoGeolocationEvent(event_name, radius_in_km, latitude, longitude, DEFAULT_UNIT_OF_MEASUREMENT) def _init_regular_updates(self): @@ -90,7 +90,7 @@ class DemoManager: self._add_entities(new_devices) -class DemoGeoLocationEvent(GeoLocationEvent): +class DemoGeolocationEvent(GeolocationEvent): """This represents a demo geolocation event.""" def __init__(self, name, distance, latitude, longitude, diff --git a/homeassistant/components/geo_location/geo_json_events.py b/homeassistant/components/geo_location/geo_json_events.py index 4d8c3b68edd..cbfe605e722 100644 --- a/homeassistant/components/geo_location/geo_json_events.py +++ b/homeassistant/components/geo_location/geo_json_events.py @@ -11,7 +11,7 @@ from typing import Optional import voluptuous as vol from homeassistant.components.geo_location import ( - PLATFORM_SCHEMA, GeoLocationEvent) + PLATFORM_SCHEMA, GeolocationEvent) from homeassistant.const import ( CONF_RADIUS, CONF_SCAN_INTERVAL, CONF_URL, EVENT_HOMEASSISTANT_START, CONF_LATITUDE, CONF_LONGITUDE) @@ -108,7 +108,7 @@ class GeoJsonFeedEntityManager: dispatcher_send(self._hass, SIGNAL_DELETE_ENTITY.format(external_id)) -class GeoJsonLocationEvent(GeoLocationEvent): +class GeoJsonLocationEvent(GeolocationEvent): """This represents an external event with GeoJSON data.""" def __init__(self, feed_manager, external_id): diff --git a/homeassistant/components/geo_location/nsw_rural_fire_service_feed.py b/homeassistant/components/geo_location/nsw_rural_fire_service_feed.py index 5681e4a53ac..e0974ed415d 100644 --- a/homeassistant/components/geo_location/nsw_rural_fire_service_feed.py +++ b/homeassistant/components/geo_location/nsw_rural_fire_service_feed.py @@ -11,7 +11,7 @@ from typing import Optional import voluptuous as vol from homeassistant.components.geo_location import ( - PLATFORM_SCHEMA, GeoLocationEvent) + PLATFORM_SCHEMA, GeolocationEvent) from homeassistant.const import ( ATTR_ATTRIBUTION, ATTR_LOCATION, CONF_RADIUS, CONF_SCAN_INTERVAL, EVENT_HOMEASSISTANT_START, CONF_LATITUDE, CONF_LONGITUDE) @@ -129,7 +129,7 @@ class NswRuralFireServiceFeedEntityManager: dispatcher_send(self._hass, SIGNAL_DELETE_ENTITY.format(external_id)) -class NswRuralFireServiceLocationEvent(GeoLocationEvent): +class NswRuralFireServiceLocationEvent(GeolocationEvent): """This represents an external event with NSW Rural Fire Service data.""" def __init__(self, feed_manager, external_id): diff --git a/homeassistant/components/geo_location/usgs_earthquakes_feed.py b/homeassistant/components/geo_location/usgs_earthquakes_feed.py index f835fecfeb4..6a7bbba4464 100644 --- a/homeassistant/components/geo_location/usgs_earthquakes_feed.py +++ b/homeassistant/components/geo_location/usgs_earthquakes_feed.py @@ -11,7 +11,7 @@ from typing import Optional import voluptuous as vol from homeassistant.components.geo_location import ( - PLATFORM_SCHEMA, GeoLocationEvent) + PLATFORM_SCHEMA, GeolocationEvent) from homeassistant.const import ( ATTR_ATTRIBUTION, CONF_RADIUS, CONF_SCAN_INTERVAL, EVENT_HOMEASSISTANT_START, CONF_LATITUDE, CONF_LONGITUDE) @@ -148,7 +148,7 @@ class UsgsEarthquakesFeedEntityManager: dispatcher_send(self._hass, SIGNAL_DELETE_ENTITY.format(external_id)) -class UsgsEarthquakesEvent(GeoLocationEvent): +class UsgsEarthquakesEvent(GeolocationEvent): """This represents an external event with USGS Earthquake data.""" def __init__(self, feed_manager, external_id): diff --git a/tests/components/automation/test_geo_location.py b/tests/components/automation/test_geo_location.py index 946c9a8abc6..928296c8d27 100644 --- a/tests/components/automation/test_geo_location.py +++ b/tests/components/automation/test_geo_location.py @@ -1,4 +1,4 @@ -"""The tests for the geo location trigger.""" +"""The tests for the geolocation trigger.""" import pytest from homeassistant.components import automation, zone diff --git a/tests/components/geo_location/test_demo.py b/tests/components/geo_location/test_demo.py index a35a8e11af6..d1c11fe55bf 100644 --- a/tests/components/geo_location/test_demo.py +++ b/tests/components/geo_location/test_demo.py @@ -39,7 +39,7 @@ class TestDemoPlatform(unittest.TestCase): with assert_setup_component(1, geo_location.DOMAIN): assert setup_component(self.hass, geo_location.DOMAIN, CONFIG) - # In this test, only entities of the geo location domain have been + # In this test, only entities of the geolocation domain have been # generated. all_states = self.hass.states.all() assert len(all_states) == NUMBER_OF_DEMO_DEVICES diff --git a/tests/components/geo_location/test_init.py b/tests/components/geo_location/test_init.py index 09030354901..a3b04848b6d 100644 --- a/tests/components/geo_location/test_init.py +++ b/tests/components/geo_location/test_init.py @@ -1,8 +1,8 @@ -"""The tests for the geo location component.""" +"""The tests for the geolocation component.""" import pytest from homeassistant.components import geo_location -from homeassistant.components.geo_location import GeoLocationEvent +from homeassistant.components.geo_location import GeolocationEvent from homeassistant.setup import async_setup_component @@ -13,8 +13,8 @@ async def test_setup_component(hass): async def test_event(hass): - """Simple test of the geo location event class.""" - entity = GeoLocationEvent() + """Simple test of the geolocation event class.""" + entity = GeolocationEvent() assert entity.state is None assert entity.distance is None From 35cb0458fa1ec5524f4d07a0b95508afca9f4836 Mon Sep 17 00:00:00 2001 From: Alistair Galbraith Date: Tue, 8 Jan 2019 20:13:47 -0800 Subject: [PATCH 061/235] Resolves #17196, Resolves #18739 - Hue Beyond light fixture errors (#19874) * Resolves #17196, Resolves #18739 - Hue Beyond light fixtures being incorrectly recognized * Removed long code lines that were failing code review * Removed trailing whitespace --- homeassistant/components/light/hue.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/light/hue.py b/homeassistant/components/light/hue.py index 686fc01caf9..28a2d79de13 100644 --- a/homeassistant/components/light/hue.py +++ b/homeassistant/components/light/hue.py @@ -298,7 +298,8 @@ class HueLight(Light): @property def device_info(self): """Return the device info.""" - if self.light.type in ('LightGroup', 'Room'): + if self.light.type in ('LightGroup', 'Room', + 'Luminaire', 'LightSource'): return None return { From b4c657a39c87787aee291818fd77c8056ca86905 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Wed, 9 Jan 2019 05:14:27 +0100 Subject: [PATCH 062/235] Update OZW to 0.1.2 (#19878) * Update ozw 0.1.2 * Update requirements_all.txt --- homeassistant/components/zwave/__init__.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zwave/__init__.py b/homeassistant/components/zwave/__init__.py index 6d96192f075..7860b545be2 100644 --- a/homeassistant/components/zwave/__init__.py +++ b/homeassistant/components/zwave/__init__.py @@ -42,7 +42,7 @@ from .discovery_schemas import DISCOVERY_SCHEMAS from .util import (check_node_schema, check_value_schema, node_name, check_has_unique_id, is_node_parsed) -REQUIREMENTS = ['pydispatcher==2.0.5', 'homeassistant-pyozw==0.1.1'] +REQUIREMENTS = ['pydispatcher==2.0.5', 'homeassistant-pyozw==0.1.2'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 7172a7027ef..6e3b5c9780a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -514,7 +514,7 @@ holidays==0.9.9 home-assistant-frontend==20181219.0 # homeassistant.components.zwave -homeassistant-pyozw==0.1.1 +homeassistant-pyozw==0.1.2 # homeassistant.components.homekit_controller # homekit==0.12.0 From 6d9fda04ac3ae4b0b0804fcc6541b45d50fb7152 Mon Sep 17 00:00:00 2001 From: Rendili <30532082+Rendili@users.noreply.github.com> Date: Wed, 9 Jan 2019 04:15:12 +0000 Subject: [PATCH 063/235] add entity support to hive (#19879) --- .../components/binary_sensor/hive.py | 19 +++++++++++++++++-- homeassistant/components/climate/hive.py | 18 +++++++++++++++++- homeassistant/components/light/hive.py | 18 +++++++++++++++++- homeassistant/components/sensor/hive.py | 18 +++++++++++++++++- homeassistant/components/switch/hive.py | 18 +++++++++++++++++- 5 files changed, 85 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/binary_sensor/hive.py b/homeassistant/components/binary_sensor/hive.py index 68f32641872..e114e67f90f 100644 --- a/homeassistant/components/binary_sensor/hive.py +++ b/homeassistant/components/binary_sensor/hive.py @@ -5,7 +5,7 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/binary_sensor.hive/ """ from homeassistant.components.binary_sensor import BinarySensorDevice -from homeassistant.components.hive import DATA_HIVE +from homeassistant.components.hive import DATA_HIVE, DOMAIN DEPENDENCIES = ['hive'] @@ -35,9 +35,24 @@ class HiveBinarySensorEntity(BinarySensorDevice): self.attributes = {} self.data_updatesource = '{}.{}'.format(self.device_type, self.node_id) - + self._unique_id = '{}-{}'.format(self.node_id, self.device_type) self.session.entities.append(self) + @property + def unique_id(self): + """Return unique ID of entity.""" + return self._unique_id + + @property + def device_info(self): + """Return device information.""" + return { + 'identifiers': { + (DOMAIN, self.unique_id) + }, + 'name': self.name + } + def handle_update(self, updatesource): """Handle the new update request.""" if '{}.{}'.format(self.device_type, self.node_id) not in updatesource: diff --git a/homeassistant/components/climate/hive.py b/homeassistant/components/climate/hive.py index 37289d45c45..87d426d6f05 100644 --- a/homeassistant/components/climate/hive.py +++ b/homeassistant/components/climate/hive.py @@ -8,7 +8,7 @@ from homeassistant.components.climate import ( ClimateDevice, STATE_AUTO, STATE_HEAT, STATE_OFF, STATE_ON, SUPPORT_AUX_HEAT, SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE) from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS -from homeassistant.components.hive import DATA_HIVE +from homeassistant.components.hive import DATA_HIVE, DOMAIN DEPENDENCIES = ['hive'] HIVE_TO_HASS_STATE = {'SCHEDULE': STATE_AUTO, 'MANUAL': STATE_HEAT, @@ -44,6 +44,7 @@ class HiveClimateEntity(ClimateDevice): self.attributes = {} self.data_updatesource = '{}.{}'.format(self.device_type, self.node_id) + self._unique_id = '{}-{}'.format(self.node_id, self.device_type) if self.device_type == "Heating": self.modes = [STATE_AUTO, STATE_HEAT, STATE_OFF] @@ -52,6 +53,21 @@ class HiveClimateEntity(ClimateDevice): self.session.entities.append(self) + @property + def unique_id(self): + """Return unique ID of entity.""" + return self._unique_id + + @property + def device_info(self): + """Return device information.""" + return { + 'identifiers': { + (DOMAIN, self.unique_id) + }, + 'name': self.name + } + @property def supported_features(self): """Return the list of supported features.""" diff --git a/homeassistant/components/light/hive.py b/homeassistant/components/light/hive.py index eada16bbab9..c2bb95f40da 100644 --- a/homeassistant/components/light/hive.py +++ b/homeassistant/components/light/hive.py @@ -4,7 +4,7 @@ Support for the Hive devices. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/light.hive/ """ -from homeassistant.components.hive import DATA_HIVE +from homeassistant.components.hive import DATA_HIVE, DOMAIN from homeassistant.components.light import (ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, SUPPORT_BRIGHTNESS, @@ -37,8 +37,24 @@ class HiveDeviceLight(Light): self.attributes = {} self.data_updatesource = '{}.{}'.format(self.device_type, self.node_id) + self._unique_id = '{}-{}'.format(self.node_id, self.device_type) self.session.entities.append(self) + @property + def unique_id(self): + """Return unique ID of entity.""" + return self._unique_id + + @property + def device_info(self): + """Return device information.""" + return { + 'identifiers': { + (DOMAIN, self.unique_id) + }, + 'name': self.name + } + def handle_update(self, updatesource): """Handle the new update request.""" if '{}.{}'.format(self.device_type, self.node_id) not in updatesource: diff --git a/homeassistant/components/sensor/hive.py b/homeassistant/components/sensor/hive.py index 3f900320801..e989074fb4b 100644 --- a/homeassistant/components/sensor/hive.py +++ b/homeassistant/components/sensor/hive.py @@ -5,7 +5,7 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.hive/ """ from homeassistant.const import TEMP_CELSIUS -from homeassistant.components.hive import DATA_HIVE +from homeassistant.components.hive import DATA_HIVE, DOMAIN from homeassistant.helpers.entity import Entity DEPENDENCIES = ['hive'] @@ -38,8 +38,24 @@ class HiveSensorEntity(Entity): self.session = hivesession self.data_updatesource = '{}.{}'.format(self.device_type, self.node_id) + self._unique_id = '{}-{}'.format(self.node_id, self.device_type) self.session.entities.append(self) + @property + def unique_id(self): + """Return unique ID of entity.""" + return self._unique_id + + @property + def device_info(self): + """Return device information.""" + return { + 'identifiers': { + (DOMAIN, self.unique_id) + }, + 'name': self.name + } + def handle_update(self, updatesource): """Handle the new update request.""" if '{}.{}'.format(self.device_type, self.node_id) not in updatesource: diff --git a/homeassistant/components/switch/hive.py b/homeassistant/components/switch/hive.py index 1927df28e97..a50323f0a4e 100644 --- a/homeassistant/components/switch/hive.py +++ b/homeassistant/components/switch/hive.py @@ -5,7 +5,7 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/switch.hive/ """ from homeassistant.components.switch import SwitchDevice -from homeassistant.components.hive import DATA_HIVE +from homeassistant.components.hive import DATA_HIVE, DOMAIN DEPENDENCIES = ['hive'] @@ -31,8 +31,24 @@ class HiveDevicePlug(SwitchDevice): self.attributes = {} self.data_updatesource = '{}.{}'.format(self.device_type, self.node_id) + self._unique_id = '{}-{}'.format(self.node_id, self.device_type) self.session.entities.append(self) + @property + def unique_id(self): + """Return unique ID of entity.""" + return self._unique_id + + @property + def device_info(self): + """Return device information.""" + return { + 'identifiers': { + (DOMAIN, self.unique_id) + }, + 'name': self.name + } + def handle_update(self, updatesource): """Handle the new update request.""" if '{}.{}'.format(self.device_type, self.node_id) not in updatesource: From 23382ab1994aaefcbf59f5bf6820cb1deb94b2fd Mon Sep 17 00:00:00 2001 From: Florian Ludwig Date: Wed, 9 Jan 2019 05:45:24 +0100 Subject: [PATCH 064/235] assign user to websocket connection when using legacy_api_password (#19797) --- homeassistant/components/websocket_api/auth.py | 4 +++- tests/components/websocket_api/test_auth.py | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/websocket_api/auth.py b/homeassistant/components/websocket_api/auth.py index 434775c9b9b..f175327bf28 100644 --- a/homeassistant/components/websocket_api/auth.py +++ b/homeassistant/components/websocket_api/auth.py @@ -6,6 +6,7 @@ from homeassistant.const import __version__ from homeassistant.components.http.auth import validate_password from homeassistant.components.http.ban import process_wrong_login, \ process_success_login +from homeassistant.auth.providers import legacy_api_password from .connection import ActiveConnection from .error import Disconnect @@ -81,7 +82,8 @@ class AuthPhase: elif self._hass.auth.support_legacy and 'api_password' in msg: self._logger.debug("Received api_password") if validate_password(self._request, msg['api_password']): - return await self._async_finish_auth(None, None) + user = await legacy_api_password.async_get_user(self._hass) + return await self._async_finish_auth(user, None) self._send_message(auth_invalid_message( 'Invalid access token or password')) diff --git a/tests/components/websocket_api/test_auth.py b/tests/components/websocket_api/test_auth.py index 4c0014e4783..9d2d2ce251e 100644 --- a/tests/components/websocket_api/test_auth.py +++ b/tests/components/websocket_api/test_auth.py @@ -132,7 +132,8 @@ async def test_auth_active_with_password_not_allow(hass, aiohttp_client): assert auth_msg['type'] == TYPE_AUTH_INVALID -async def test_auth_legacy_support_with_password(hass, aiohttp_client): +async def test_auth_legacy_support_with_password(hass, aiohttp_client, + legacy_auth): """Test authenticating with a token.""" assert await async_setup_component(hass, 'websocket_api', { 'http': { From 2bdbf6955d5a4e808997c843a68e72a483f94595 Mon Sep 17 00:00:00 2001 From: Rohan Kapoor Date: Tue, 8 Jan 2019 20:47:05 -0800 Subject: [PATCH 065/235] Migrate geofency over to the Webhook component (#18951) * Migrate geofency over to the Webhook component * Return web.Response correctly * Fix test * Lint * Fix error that tests caught --- .../components/geofency/.translations/en.json | 18 ++ homeassistant/components/geofency/__init__.py | 193 ++++++++++-------- .../components/geofency/strings.json | 18 ++ homeassistant/config_entries.py | 1 + tests/components/geofency/test_init.py | 62 ++++-- 5 files changed, 186 insertions(+), 106 deletions(-) create mode 100644 homeassistant/components/geofency/.translations/en.json create mode 100644 homeassistant/components/geofency/strings.json diff --git a/homeassistant/components/geofency/.translations/en.json b/homeassistant/components/geofency/.translations/en.json new file mode 100644 index 00000000000..e67af592c16 --- /dev/null +++ b/homeassistant/components/geofency/.translations/en.json @@ -0,0 +1,18 @@ +{ + "config": { + "title": "Geofency Webhook", + "step": { + "user": { + "title": "Set up the Geofency Webhook", + "description": "Are you sure you want to set up the Geofency Webhook?" + } + }, + "abort": { + "one_instance_allowed": "Only a single instance is necessary.", + "not_internet_accessible": "Your Home Assistant instance needs to be accessible from the internet to receive messages from Geofency." + }, + "create_entry": { + "default": "To send events to Home Assistant, you will need to setup the webhook feature in Geofency.\n\nFill in the following info:\n\n- URL: `{webhook_url}`\n- Method: POST\n\nSee [the documentation]({docs_url}) for further details." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/geofency/__init__.py b/homeassistant/components/geofency/__init__.py index 92f8f475e65..1e2f368d5b3 100644 --- a/homeassistant/components/geofency/__init__.py +++ b/homeassistant/components/geofency/__init__.py @@ -7,11 +7,12 @@ https://home-assistant.io/components/geofency/ import logging import voluptuous as vol +from aiohttp import web import homeassistant.helpers.config_validation as cv -from homeassistant.components.http import HomeAssistantView from homeassistant.const import HTTP_UNPROCESSABLE_ENTITY, STATE_NOT_HOME, \ - ATTR_LATITUDE, ATTR_LONGITUDE + ATTR_LATITUDE, ATTR_LONGITUDE, CONF_WEBHOOK_ID, HTTP_OK +from homeassistant.helpers import config_entry_flow from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.util import slugify @@ -19,7 +20,7 @@ from homeassistant.util import slugify _LOGGER = logging.getLogger(__name__) DOMAIN = 'geofency' -DEPENDENCIES = ['http'] +DEPENDENCIES = ['webhook'] CONF_MOBILE_BEACONS = 'mobile_beacons' @@ -40,8 +41,6 @@ BEACON_DEV_PREFIX = 'beacon' LOCATION_ENTRY = '1' LOCATION_EXIT = '0' -URL = '/api/geofency' - TRACKER_UPDATE = '{}_tracker_update'.format(DOMAIN) @@ -50,7 +49,6 @@ async def async_setup(hass, hass_config): config = hass_config[DOMAIN] mobile_beacons = config[CONF_MOBILE_BEACONS] hass.data[DOMAIN] = [slugify(beacon) for beacon in mobile_beacons] - hass.http.register_view(GeofencyView(hass.data[DOMAIN])) hass.async_create_task( async_load_platform(hass, 'device_tracker', DOMAIN, {}, hass_config) @@ -58,89 +56,106 @@ async def async_setup(hass, hass_config): return True -class GeofencyView(HomeAssistantView): - """View to handle Geofency requests.""" +async def handle_webhook(hass, webhook_id, request): + """Handle incoming webhook with Mailgun inbound messages.""" + data = _validate_data(await request.post()) - url = URL - name = 'api:geofency' - - def __init__(self, mobile_beacons): - """Initialize Geofency url endpoints.""" - self.mobile_beacons = mobile_beacons - - async def post(self, request): - """Handle Geofency requests.""" - data = await request.post() - hass = request.app['hass'] - - data = self._validate_data(data) - if not data: - return "Invalid data", HTTP_UNPROCESSABLE_ENTITY - - if self._is_mobile_beacon(data): - return await self._set_location(hass, data, None) - if data['entry'] == LOCATION_ENTRY: - location_name = data['name'] - else: - location_name = STATE_NOT_HOME - if ATTR_CURRENT_LATITUDE in data: - data[ATTR_LATITUDE] = data[ATTR_CURRENT_LATITUDE] - data[ATTR_LONGITUDE] = data[ATTR_CURRENT_LONGITUDE] - - return await self._set_location(hass, data, location_name) - - @staticmethod - def _validate_data(data): - """Validate POST payload.""" - data = data.copy() - - required_attributes = ['address', 'device', 'entry', - 'latitude', 'longitude', 'name'] - - valid = True - for attribute in required_attributes: - if attribute not in data: - valid = False - _LOGGER.error("'%s' not specified in message", attribute) - - if not valid: - return {} - - data['address'] = data['address'].replace('\n', ' ') - data['device'] = slugify(data['device']) - data['name'] = slugify(data['name']) - - gps_attributes = [ATTR_LATITUDE, ATTR_LONGITUDE, - ATTR_CURRENT_LATITUDE, ATTR_CURRENT_LONGITUDE] - - for attribute in gps_attributes: - if attribute in data: - data[attribute] = float(data[attribute]) - - return data - - def _is_mobile_beacon(self, data): - """Check if we have a mobile beacon.""" - return 'beaconUUID' in data and data['name'] in self.mobile_beacons - - @staticmethod - def _device_name(data): - """Return name of device tracker.""" - if 'beaconUUID' in data: - return "{}_{}".format(BEACON_DEV_PREFIX, data['name']) - return data['device'] - - async def _set_location(self, hass, data, location_name): - """Fire HA event to set location.""" - device = self._device_name(data) - - async_dispatcher_send( - hass, - TRACKER_UPDATE, - device, - (data[ATTR_LATITUDE], data[ATTR_LONGITUDE]), - location_name, - data + if not data: + return web.Response( + body="Invalid data", + status=HTTP_UNPROCESSABLE_ENTITY ) - return "Setting location for {}".format(device) + if _is_mobile_beacon(data, hass.data[DOMAIN]): + return _set_location(hass, data, None) + if data['entry'] == LOCATION_ENTRY: + location_name = data['name'] + else: + location_name = STATE_NOT_HOME + if ATTR_CURRENT_LATITUDE in data: + data[ATTR_LATITUDE] = data[ATTR_CURRENT_LATITUDE] + data[ATTR_LONGITUDE] = data[ATTR_CURRENT_LONGITUDE] + + return _set_location(hass, data, location_name) + + +def _validate_data(data): + """Validate POST payload.""" + data = data.copy() + + required_attributes = ['address', 'device', 'entry', + 'latitude', 'longitude', 'name'] + + valid = True + for attribute in required_attributes: + if attribute not in data: + valid = False + _LOGGER.error("'%s' not specified in message", attribute) + + if not valid: + return {} + + data['address'] = data['address'].replace('\n', ' ') + data['device'] = slugify(data['device']) + data['name'] = slugify(data['name']) + + gps_attributes = [ATTR_LATITUDE, ATTR_LONGITUDE, + ATTR_CURRENT_LATITUDE, ATTR_CURRENT_LONGITUDE] + + for attribute in gps_attributes: + if attribute in data: + data[attribute] = float(data[attribute]) + + return data + + +def _is_mobile_beacon(data, mobile_beacons): + """Check if we have a mobile beacon.""" + return 'beaconUUID' in data and data['name'] in mobile_beacons + + +def _device_name(data): + """Return name of device tracker.""" + if 'beaconUUID' in data: + return "{}_{}".format(BEACON_DEV_PREFIX, data['name']) + return data['device'] + + +def _set_location(hass, data, location_name): + """Fire HA event to set location.""" + device = _device_name(data) + + async_dispatcher_send( + hass, + TRACKER_UPDATE, + device, + (data[ATTR_LATITUDE], data[ATTR_LONGITUDE]), + location_name, + data + ) + + return web.Response( + body="Setting location for {}".format(device), + status=HTTP_OK + ) + + +async def async_setup_entry(hass, entry): + """Configure based on config entry.""" + hass.components.webhook.async_register( + DOMAIN, 'Geofency', entry.data[CONF_WEBHOOK_ID], handle_webhook) + return True + + +async def async_unload_entry(hass, entry): + """Unload a config entry.""" + hass.components.webhook.async_unregister(entry.data[CONF_WEBHOOK_ID]) + return True + +config_entry_flow.register_webhook_flow( + DOMAIN, + 'Geofency Webhook', + { + 'docs_url': 'https://www.home-assistant.io/components/geofency/' + } +) diff --git a/homeassistant/components/geofency/strings.json b/homeassistant/components/geofency/strings.json new file mode 100644 index 00000000000..e67af592c16 --- /dev/null +++ b/homeassistant/components/geofency/strings.json @@ -0,0 +1,18 @@ +{ + "config": { + "title": "Geofency Webhook", + "step": { + "user": { + "title": "Set up the Geofency Webhook", + "description": "Are you sure you want to set up the Geofency Webhook?" + } + }, + "abort": { + "one_instance_allowed": "Only a single instance is necessary.", + "not_internet_accessible": "Your Home Assistant instance needs to be accessible from the internet to receive messages from Geofency." + }, + "create_entry": { + "default": "To send events to Home Assistant, you will need to setup the webhook feature in Geofency.\n\nFill in the following info:\n\n- URL: `{webhook_url}`\n- Method: POST\n\nSee [the documentation]({docs_url}) for further details." + } + } +} \ No newline at end of file diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 2048b4214c6..8af366ce604 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -140,6 +140,7 @@ FLOWS = [ 'deconz', 'dialogflow', 'esphome', + 'geofency', 'hangouts', 'homematicip_cloud', 'hue', diff --git a/tests/components/geofency/test_init.py b/tests/components/geofency/test_init.py index ae90af61ced..fa1829e2d68 100644 --- a/tests/components/geofency/test_init.py +++ b/tests/components/geofency/test_init.py @@ -1,12 +1,13 @@ """The tests for the Geofency device tracker platform.""" # pylint: disable=redefined-outer-name -from unittest.mock import patch +from unittest.mock import patch, Mock import pytest +from homeassistant import data_entry_flow from homeassistant.components import zone from homeassistant.components.geofency import ( - CONF_MOBILE_BEACONS, URL, DOMAIN) + CONF_MOBILE_BEACONS, DOMAIN) from homeassistant.const import ( HTTP_OK, HTTP_UNPROCESSABLE_ENTITY, STATE_HOME, STATE_NOT_HOME) @@ -113,6 +114,9 @@ def mock_dev_track(mock_device_tracker_conf): @pytest.fixture def geofency_client(loop, hass, hass_client): """Geofency mock client.""" + assert loop.run_until_complete(async_setup_component( + hass, 'persistent_notification', {})) + assert loop.run_until_complete(async_setup_component( hass, DOMAIN, { DOMAIN: { @@ -138,10 +142,28 @@ def setup_zones(loop, hass): }})) -async def test_data_validation(geofency_client): +@pytest.fixture +async def webhook_id(hass, geofency_client): + """Initialize the Geofency component and get the webhook_id.""" + hass.config.api = Mock(base_url='http://example.com') + result = await hass.config_entries.flow.async_init('geofency', context={ + 'source': 'user' + }) + assert result['type'] == data_entry_flow.RESULT_TYPE_FORM, result + + result = await hass.config_entries.flow.async_configure( + result['flow_id'], {}) + assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + + return result['result'].data['webhook_id'] + + +async def test_data_validation(geofency_client, webhook_id): """Test data validation.""" + url = '/api/webhook/{}'.format(webhook_id) + # No data - req = await geofency_client.post(URL) + req = await geofency_client.post(url) assert req.status == HTTP_UNPROCESSABLE_ENTITY missing_attributes = ['address', 'device', @@ -151,14 +173,16 @@ async def test_data_validation(geofency_client): for attribute in missing_attributes: copy = GPS_ENTER_HOME.copy() del copy[attribute] - req = await geofency_client.post(URL, data=copy) + req = await geofency_client.post(url, data=copy) assert req.status == HTTP_UNPROCESSABLE_ENTITY -async def test_gps_enter_and_exit_home(hass, geofency_client): +async def test_gps_enter_and_exit_home(hass, geofency_client, webhook_id): """Test GPS based zone enter and exit.""" + url = '/api/webhook/{}'.format(webhook_id) + # Enter the Home zone - req = await geofency_client.post(URL, data=GPS_ENTER_HOME) + req = await geofency_client.post(url, data=GPS_ENTER_HOME) await hass.async_block_till_done() assert req.status == HTTP_OK device_name = slugify(GPS_ENTER_HOME['device']) @@ -167,7 +191,7 @@ async def test_gps_enter_and_exit_home(hass, geofency_client): assert STATE_HOME == state_name # Exit the Home zone - req = await geofency_client.post(URL, data=GPS_EXIT_HOME) + req = await geofency_client.post(url, data=GPS_EXIT_HOME) await hass.async_block_till_done() assert req.status == HTTP_OK device_name = slugify(GPS_EXIT_HOME['device']) @@ -180,7 +204,7 @@ async def test_gps_enter_and_exit_home(hass, geofency_client): data['currentLatitude'] = NOT_HOME_LATITUDE data['currentLongitude'] = NOT_HOME_LONGITUDE - req = await geofency_client.post(URL, data=data) + req = await geofency_client.post(url, data=data) await hass.async_block_till_done() assert req.status == HTTP_OK device_name = slugify(GPS_EXIT_HOME['device']) @@ -192,10 +216,12 @@ async def test_gps_enter_and_exit_home(hass, geofency_client): assert NOT_HOME_LONGITUDE == current_longitude -async def test_beacon_enter_and_exit_home(hass, geofency_client): +async def test_beacon_enter_and_exit_home(hass, geofency_client, webhook_id): """Test iBeacon based zone enter and exit - a.k.a stationary iBeacon.""" + url = '/api/webhook/{}'.format(webhook_id) + # Enter the Home zone - req = await geofency_client.post(URL, data=BEACON_ENTER_HOME) + req = await geofency_client.post(url, data=BEACON_ENTER_HOME) await hass.async_block_till_done() assert req.status == HTTP_OK device_name = slugify("beacon_{}".format(BEACON_ENTER_HOME['name'])) @@ -204,7 +230,7 @@ async def test_beacon_enter_and_exit_home(hass, geofency_client): assert STATE_HOME == state_name # Exit the Home zone - req = await geofency_client.post(URL, data=BEACON_EXIT_HOME) + req = await geofency_client.post(url, data=BEACON_EXIT_HOME) await hass.async_block_till_done() assert req.status == HTTP_OK device_name = slugify("beacon_{}".format(BEACON_ENTER_HOME['name'])) @@ -213,10 +239,12 @@ async def test_beacon_enter_and_exit_home(hass, geofency_client): assert STATE_NOT_HOME == state_name -async def test_beacon_enter_and_exit_car(hass, geofency_client): +async def test_beacon_enter_and_exit_car(hass, geofency_client, webhook_id): """Test use of mobile iBeacon.""" + url = '/api/webhook/{}'.format(webhook_id) + # Enter the Car away from Home zone - req = await geofency_client.post(URL, data=BEACON_ENTER_CAR) + req = await geofency_client.post(url, data=BEACON_ENTER_CAR) await hass.async_block_till_done() assert req.status == HTTP_OK device_name = slugify("beacon_{}".format(BEACON_ENTER_CAR['name'])) @@ -225,7 +253,7 @@ async def test_beacon_enter_and_exit_car(hass, geofency_client): assert STATE_NOT_HOME == state_name # Exit the Car away from Home zone - req = await geofency_client.post(URL, data=BEACON_EXIT_CAR) + req = await geofency_client.post(url, data=BEACON_EXIT_CAR) await hass.async_block_till_done() assert req.status == HTTP_OK device_name = slugify("beacon_{}".format(BEACON_ENTER_CAR['name'])) @@ -237,7 +265,7 @@ async def test_beacon_enter_and_exit_car(hass, geofency_client): data = BEACON_ENTER_CAR.copy() data['latitude'] = HOME_LATITUDE data['longitude'] = HOME_LONGITUDE - req = await geofency_client.post(URL, data=data) + req = await geofency_client.post(url, data=data) await hass.async_block_till_done() assert req.status == HTTP_OK device_name = slugify("beacon_{}".format(data['name'])) @@ -246,7 +274,7 @@ async def test_beacon_enter_and_exit_car(hass, geofency_client): assert STATE_HOME == state_name # Exit the Car in the Home zone - req = await geofency_client.post(URL, data=data) + req = await geofency_client.post(url, data=data) await hass.async_block_till_done() assert req.status == HTTP_OK device_name = slugify("beacon_{}".format(data['name'])) From 64b4c8f43a501f005f60a3bf3159d29722af3d94 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 8 Jan 2019 21:09:47 -0800 Subject: [PATCH 066/235] Fix deprecation warning (#19882) --- homeassistant/helpers/aiohttp_client.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/helpers/aiohttp_client.py b/homeassistant/helpers/aiohttp_client.py index 53b246c700d..b2669312e38 100644 --- a/homeassistant/helpers/aiohttp_client.py +++ b/homeassistant/helpers/aiohttp_client.py @@ -155,10 +155,9 @@ def _async_get_connector(hass, verify_ssl=True): connector = aiohttp.TCPConnector(loop=hass.loop, ssl=ssl_context) hass.data[key] = connector - @callback - def _async_close_connector(event): + async def _async_close_connector(event): """Close connector pool.""" - connector.close() + await connector.close() hass.bus.async_listen_once( EVENT_HOMEASSISTANT_CLOSE, _async_close_connector) From 4394e37df9d0e9ac5be47e768f78371c93e03079 Mon Sep 17 00:00:00 2001 From: Rendili <30532082+Rendili@users.noreply.github.com> Date: Wed, 9 Jan 2019 10:21:03 +0000 Subject: [PATCH 067/235] Bug fix with getting a device battery level when API unavailable for Hive (#19841) * hive updates - bug fix and add entity registration * remove hive entity registration code --- homeassistant/components/hive.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/hive.py b/homeassistant/components/hive.py index aa662fc2fb6..09319849933 100644 --- a/homeassistant/components/hive.py +++ b/homeassistant/components/hive.py @@ -12,7 +12,7 @@ from homeassistant.const import (CONF_PASSWORD, CONF_SCAN_INTERVAL, import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import load_platform -REQUIREMENTS = ['pyhiveapi==0.2.14'] +REQUIREMENTS = ['pyhiveapi==0.2.17'] _LOGGER = logging.getLogger(__name__) DOMAIN = 'hive' diff --git a/requirements_all.txt b/requirements_all.txt index 6e3b5c9780a..9fd6822f1dc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1019,7 +1019,7 @@ pyhaversion==2.0.3 pyhik==0.1.9 # homeassistant.components.hive -pyhiveapi==0.2.14 +pyhiveapi==0.2.17 # homeassistant.components.homematic pyhomematic==0.1.54 From 5cab319798904d4ef2b2fe7ec0e9eca621a0c94e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 9 Jan 2019 15:12:29 -0800 Subject: [PATCH 068/235] Updated frontend to 20190109.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 475ce26bc43..478ff5549d9 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -24,7 +24,7 @@ from homeassistant.core import callback from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass -REQUIREMENTS = ['home-assistant-frontend==20181219.0'] +REQUIREMENTS = ['home-assistant-frontend==20190109.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log', diff --git a/requirements_all.txt b/requirements_all.txt index 9fd6822f1dc..8f4a916d547 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -511,7 +511,7 @@ hole==0.3.0 holidays==0.9.9 # homeassistant.components.frontend -home-assistant-frontend==20181219.0 +home-assistant-frontend==20190109.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7199207f567..cb533e35227 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -104,7 +104,7 @@ hdate==0.7.5 holidays==0.9.9 # homeassistant.components.frontend -home-assistant-frontend==20181219.0 +home-assistant-frontend==20190109.0 # homeassistant.components.homematicip_cloud homematicip==0.10.3 From 9aed40a88d0ed7b93509c454cf0b72e9802175f0 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 9 Jan 2019 15:29:31 -0800 Subject: [PATCH 069/235] Update translations --- .../components/auth/.translations/de.json | 4 +-- .../components/daikin/.translations/de.json | 19 +++++++++++ .../components/daikin/.translations/hu.json | 11 +++++++ .../components/daikin/.translations/nl.json | 19 +++++++++++ .../components/daikin/.translations/no.json | 19 +++++++++++ .../components/daikin/.translations/pl.json | 19 +++++++++++ .../daikin/.translations/pt-BR.json | 16 ++++++++++ .../components/daikin/.translations/pt.json | 19 +++++++++++ .../components/deconz/.translations/de.json | 2 +- .../components/deconz/.translations/sl.json | 4 +-- .../dialogflow/.translations/de.json | 8 +++++ .../dialogflow/.translations/es.json | 3 ++ .../dialogflow/.translations/nl.json | 3 ++ .../components/esphome/.translations/de.json | 30 +++++++++++++++++ .../components/esphome/.translations/fr.json | 21 ++++++++++++ .../components/esphome/.translations/hu.json | 23 +++++++++++++ .../components/esphome/.translations/id.json | 16 ++++++++++ .../components/esphome/.translations/ko.json | 2 +- .../components/esphome/.translations/nl.json | 30 +++++++++++++++++ .../components/esphome/.translations/no.json | 30 +++++++++++++++++ .../components/esphome/.translations/pl.json | 30 +++++++++++++++++ .../esphome/.translations/pt-BR.json | 29 +++++++++++++++++ .../components/esphome/.translations/pt.json | 30 +++++++++++++++++ .../components/geofency/.translations/en.json | 30 ++++++++--------- .../components/geofency/.translations/lb.json | 18 +++++++++++ .../components/hangouts/.translations/de.json | 2 +- .../hangouts/.translations/pt-BR.json | 3 ++ .../components/hangouts/.translations/sl.json | 4 +-- .../components/ifttt/.translations/pt-BR.json | 11 +++++++ .../components/lifx/.translations/pt-BR.json | 15 +++++++++ .../luftdaten/.translations/de.json | 8 +++-- .../luftdaten/.translations/pt-BR.json | 14 ++++++++ .../luftdaten/.translations/pt.json | 9 ++++-- .../components/mailgun/.translations/de.json | 18 +++++++++++ .../components/mqtt/.translations/pt-BR.json | 6 ++++ .../components/mqtt/.translations/uk.json | 6 +++- .../owntracks/.translations/de.json | 17 ++++++++++ .../owntracks/.translations/nl.json | 17 ++++++++++ .../owntracks/.translations/uk.json | 13 ++++++++ .../components/point/.translations/de.json | 32 +++++++++++++++++++ .../components/point/.translations/nl.json | 21 +++++++++++- .../components/point/.translations/pt-BR.json | 31 ++++++++++++++++++ .../components/point/.translations/pt.json | 24 ++++++++++++-- .../rainmachine/.translations/hu.json | 4 ++- .../rainmachine/.translations/pt-BR.json | 19 +++++++++++ .../rainmachine/.translations/pt.json | 8 ++++- .../sensor/.translations/moon.uk.json | 12 +++++++ .../simplisafe/.translations/pt-BR.json | 4 ++- .../components/smhi/.translations/pt-BR.json | 9 ++++-- .../tellduslive/.translations/de.json | 23 +++++++++++++ .../tellduslive/.translations/fr.json | 9 ++++++ .../tellduslive/.translations/hu.json | 8 +++++ .../tellduslive/.translations/nl.json | 24 ++++++++++++++ .../tellduslive/.translations/pl.json | 1 + .../tellduslive/.translations/pt-BR.json | 17 ++++++++++ .../tellduslive/.translations/pt.json | 24 ++++++++++++++ .../components/twilio/.translations/de.json | 13 ++++++++ .../components/twilio/.translations/es.json | 3 +- .../components/unifi/.translations/de.json | 16 ++++++++-- .../components/unifi/.translations/pt-BR.json | 5 +++ .../components/upnp/.translations/ca.json | 8 ++++- .../components/upnp/.translations/de.json | 9 +++++- .../components/upnp/.translations/en.json | 8 ++++- .../components/upnp/.translations/es.json | 4 +++ .../components/upnp/.translations/hu.json | 7 ++++ .../components/upnp/.translations/ko.json | 8 ++++- .../components/upnp/.translations/lb.json | 8 ++++- .../components/upnp/.translations/nl.json | 14 +++++--- .../components/upnp/.translations/no.json | 8 ++++- .../components/upnp/.translations/pl.json | 8 ++++- .../components/upnp/.translations/pt-BR.json | 28 ++++++++++++++++ .../components/upnp/.translations/pt.json | 8 ++++- .../components/upnp/.translations/ru.json | 8 ++++- .../components/upnp/.translations/sl.json | 8 ++++- .../components/upnp/.translations/uk.json | 18 +++++++++++ .../upnp/.translations/zh-Hant.json | 8 ++++- .../components/zha/.translations/de.json | 20 ++++++++++++ .../components/zha/.translations/nl.json | 6 ++++ .../components/zha/.translations/pt-BR.json | 18 +++++++++++ .../components/zha/.translations/pt.json | 6 ++-- .../components/zwave/.translations/pt-BR.json | 17 ++++++++++ 81 files changed, 1051 insertions(+), 61 deletions(-) create mode 100644 homeassistant/components/daikin/.translations/de.json create mode 100644 homeassistant/components/daikin/.translations/hu.json create mode 100644 homeassistant/components/daikin/.translations/nl.json create mode 100644 homeassistant/components/daikin/.translations/no.json create mode 100644 homeassistant/components/daikin/.translations/pl.json create mode 100644 homeassistant/components/daikin/.translations/pt-BR.json create mode 100644 homeassistant/components/daikin/.translations/pt.json create mode 100644 homeassistant/components/esphome/.translations/de.json create mode 100644 homeassistant/components/esphome/.translations/fr.json create mode 100644 homeassistant/components/esphome/.translations/hu.json create mode 100644 homeassistant/components/esphome/.translations/id.json create mode 100644 homeassistant/components/esphome/.translations/nl.json create mode 100644 homeassistant/components/esphome/.translations/no.json create mode 100644 homeassistant/components/esphome/.translations/pl.json create mode 100644 homeassistant/components/esphome/.translations/pt-BR.json create mode 100644 homeassistant/components/esphome/.translations/pt.json create mode 100644 homeassistant/components/geofency/.translations/lb.json create mode 100644 homeassistant/components/ifttt/.translations/pt-BR.json create mode 100644 homeassistant/components/lifx/.translations/pt-BR.json create mode 100644 homeassistant/components/luftdaten/.translations/pt-BR.json create mode 100644 homeassistant/components/mailgun/.translations/de.json create mode 100644 homeassistant/components/owntracks/.translations/de.json create mode 100644 homeassistant/components/owntracks/.translations/nl.json create mode 100644 homeassistant/components/owntracks/.translations/uk.json create mode 100644 homeassistant/components/point/.translations/de.json create mode 100644 homeassistant/components/point/.translations/pt-BR.json create mode 100644 homeassistant/components/rainmachine/.translations/pt-BR.json create mode 100644 homeassistant/components/sensor/.translations/moon.uk.json create mode 100644 homeassistant/components/tellduslive/.translations/de.json create mode 100644 homeassistant/components/tellduslive/.translations/fr.json create mode 100644 homeassistant/components/tellduslive/.translations/nl.json create mode 100644 homeassistant/components/tellduslive/.translations/pt-BR.json create mode 100644 homeassistant/components/tellduslive/.translations/pt.json create mode 100644 homeassistant/components/upnp/.translations/pt-BR.json create mode 100644 homeassistant/components/upnp/.translations/uk.json create mode 100644 homeassistant/components/zha/.translations/de.json create mode 100644 homeassistant/components/zha/.translations/pt-BR.json diff --git a/homeassistant/components/auth/.translations/de.json b/homeassistant/components/auth/.translations/de.json index 4ef4a5bf9e8..06da3cde1a1 100644 --- a/homeassistant/components/auth/.translations/de.json +++ b/homeassistant/components/auth/.translations/de.json @@ -9,11 +9,11 @@ }, "step": { "init": { - "description": "Bitte w\u00e4hle einen der Benachrichtigungsdienste:", + "description": "Bitte w\u00e4hlen Sie einen der Benachrichtigungsdienste:", "title": "Einmal Passwort f\u00fcr Notify einrichten" }, "setup": { - "description": "Ein Einmal-Passwort wurde per ** notify gesendet. {notify_service} **. Bitte gebe es unten ein:", + "description": "Ein Einmal-Passwort wurde per **notify.{notify_service}** gesendet. Bitte geben Sie es unten ein:", "title": "\u00dcberpr\u00fcfe das Setup" } }, diff --git a/homeassistant/components/daikin/.translations/de.json b/homeassistant/components/daikin/.translations/de.json new file mode 100644 index 00000000000..0a09c7b5cfa --- /dev/null +++ b/homeassistant/components/daikin/.translations/de.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "device_fail": "Unerwarteter Fehler beim Erstellen des Ger\u00e4ts.", + "device_timeout": "Zeit\u00fcberschreitung beim Verbinden mit dem Ger\u00e4t." + }, + "step": { + "user": { + "data": { + "host": "Host" + }, + "description": "Geben Sie die IP-Adresse Ihrer Daikin AC ein.", + "title": "Daikin AC konfigurieren" + } + }, + "title": "Daikin AC" + } +} \ No newline at end of file diff --git a/homeassistant/components/daikin/.translations/hu.json b/homeassistant/components/daikin/.translations/hu.json new file mode 100644 index 00000000000..623fab6828a --- /dev/null +++ b/homeassistant/components/daikin/.translations/hu.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Kiszolg\u00e1l\u00f3" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/daikin/.translations/nl.json b/homeassistant/components/daikin/.translations/nl.json new file mode 100644 index 00000000000..683bb61dd44 --- /dev/null +++ b/homeassistant/components/daikin/.translations/nl.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd", + "device_fail": "Onverwachte fout bij het aanmaken van een apparaat.", + "device_timeout": "Time-out voor verbinding met het apparaat." + }, + "step": { + "user": { + "data": { + "host": "Host" + }, + "description": "Voer het IP-adres van uw Daikin AC in.", + "title": "Daikin AC instellen" + } + }, + "title": "Daikin AC" + } +} \ No newline at end of file diff --git a/homeassistant/components/daikin/.translations/no.json b/homeassistant/components/daikin/.translations/no.json new file mode 100644 index 00000000000..806106c5e52 --- /dev/null +++ b/homeassistant/components/daikin/.translations/no.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert", + "device_fail": "Uventet feil under oppretting av enheten.", + "device_timeout": "Tidsavbrudd for tilkobling til enheten." + }, + "step": { + "user": { + "data": { + "host": "Vert" + }, + "description": "Angi IP-adressen til din Daikin AC.", + "title": "Konfigurer Daikin AC" + } + }, + "title": "Daikin AC" + } +} \ No newline at end of file diff --git a/homeassistant/components/daikin/.translations/pl.json b/homeassistant/components/daikin/.translations/pl.json new file mode 100644 index 00000000000..49c5a497667 --- /dev/null +++ b/homeassistant/components/daikin/.translations/pl.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "device_fail": "Nieoczekiwany b\u0142\u0105d tworzenia urz\u0105dzenia.", + "device_timeout": "Limit czasu pod\u0142\u0105czenia do urz\u0105dzenia." + }, + "step": { + "user": { + "data": { + "host": "Host" + }, + "description": "Wprowad\u017a adres IP Daikin AC.", + "title": "Konfiguracja Daikin AC" + } + }, + "title": "Daikin AC" + } +} \ No newline at end of file diff --git a/homeassistant/components/daikin/.translations/pt-BR.json b/homeassistant/components/daikin/.translations/pt-BR.json new file mode 100644 index 00000000000..58c5a9c77b2 --- /dev/null +++ b/homeassistant/components/daikin/.translations/pt-BR.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "device_fail": "Erro inesperado ao criar dispositivo.", + "device_timeout": "Excedido tempo limite conectando ao dispositivo" + }, + "step": { + "user": { + "description": "Digite o endere\u00e7o IP do seu AC Daikin.", + "title": "Configurar o AC Daikin" + } + }, + "title": "AC Daikin" + } +} \ No newline at end of file diff --git a/homeassistant/components/daikin/.translations/pt.json b/homeassistant/components/daikin/.translations/pt.json new file mode 100644 index 00000000000..34b4c86e77d --- /dev/null +++ b/homeassistant/components/daikin/.translations/pt.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "device_fail": "Erro inesperado ao criar dispositivo.", + "device_timeout": "Tempo excedido a tentar ligar ao dispositivo." + }, + "step": { + "user": { + "data": { + "host": "Servidor" + }, + "description": "Introduza o endere\u00e7o IP do seu Daikin AC.", + "title": "Configurar o Daikin AC" + } + }, + "title": "Daikin AC" + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/de.json b/homeassistant/components/deconz/.translations/de.json index 645daa56f6b..dce2c7a6704 100644 --- a/homeassistant/components/deconz/.translations/de.json +++ b/homeassistant/components/deconz/.translations/de.json @@ -12,7 +12,7 @@ "init": { "data": { "host": "Host", - "port": "Port (Standartwert : '80')" + "port": "Port" }, "title": "Definiere das deCONZ-Gateway" }, diff --git a/homeassistant/components/deconz/.translations/sl.json b/homeassistant/components/deconz/.translations/sl.json index bc7a2cbd861..cea6f8ef4dd 100644 --- a/homeassistant/components/deconz/.translations/sl.json +++ b/homeassistant/components/deconz/.translations/sl.json @@ -12,7 +12,7 @@ "init": { "data": { "host": "Gostitelj", - "port": "Vrata (privzeta vrednost: '80')" + "port": "Vrata" }, "title": "Dolo\u010dite deCONZ prehod" }, @@ -28,6 +28,6 @@ "title": "Dodatne mo\u017enosti konfiguracije za deCONZ" } }, - "title": "deCONZ" + "title": "deCONZ Zigbee prehod" } } \ No newline at end of file diff --git a/homeassistant/components/dialogflow/.translations/de.json b/homeassistant/components/dialogflow/.translations/de.json index e10d890b501..f585799391e 100644 --- a/homeassistant/components/dialogflow/.translations/de.json +++ b/homeassistant/components/dialogflow/.translations/de.json @@ -1,7 +1,15 @@ { "config": { + "abort": { + "not_internet_accessible": "Ihre Home Assistant-Instanz muss \u00fcber das Internet erreichbar sein, um Dialogflow-Nachrichten empfangen zu k\u00f6nnen.", + "one_instance_allowed": "Nur eine einzige Instanz ist notwendig." + }, + "create_entry": { + "default": "Um Ereignisse an den Home Assistant zu senden, m\u00fcssen Sie [Webhook-Integration von Dialogflow]({dialogflow_url}) einrichten. \n\nF\u00fcllen Sie die folgenden Informationen aus: \n\n - URL: ` {webhook_url} ` \n - Methode: POST \n - Inhaltstyp: application / json \n\nWeitere Informationen finden Sie in der [Dokumentation]({docs_url})." + }, "step": { "user": { + "description": "M\u00f6chten Sie Dialogflow wirklich einrichten?", "title": "Dialogflow Webhook einrichten" } }, diff --git a/homeassistant/components/dialogflow/.translations/es.json b/homeassistant/components/dialogflow/.translations/es.json index 892f0c5bfd0..ee07635de4a 100644 --- a/homeassistant/components/dialogflow/.translations/es.json +++ b/homeassistant/components/dialogflow/.translations/es.json @@ -4,6 +4,9 @@ "not_internet_accessible": "Su instancia de Home Assistant debe ser accesible desde Internet para recibir mensajes de Dialogflow.", "one_instance_allowed": "Solo una instancia es necesaria." }, + "create_entry": { + "default": "Para enviar eventos a Home Assistant, necesitas configurar [Integracion de flujos de dialogo de webhook]({dialogflow_url}).\n\nRellena la siguiente informaci\u00f3n:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\nVer [Documentaci\u00f3n]({docs_url}) para mas detalles." + }, "step": { "user": { "description": "\u00bfEst\u00e1 seguro de que desea configurar Dialogflow?" diff --git a/homeassistant/components/dialogflow/.translations/nl.json b/homeassistant/components/dialogflow/.translations/nl.json index 5a28d6be9ac..9871df0d262 100644 --- a/homeassistant/components/dialogflow/.translations/nl.json +++ b/homeassistant/components/dialogflow/.translations/nl.json @@ -4,6 +4,9 @@ "not_internet_accessible": "Uw Home Assistant instantie moet toegankelijk zijn vanaf het internet om Dialogflow-berichten te ontvangen.", "one_instance_allowed": "Slechts \u00e9\u00e9n instantie is nodig." }, + "create_entry": { + "default": "Om evenementen naar de Home Assistant te verzenden, moet u [webhookintegratie van Dialogflow]({dialogflow_url}) instellen. \n\n Vul de volgende info in: \n\n - URL: `{webhook_url}` \n - Method: POST \n - Content Type: application/json \n\nZie [de documentatie]({docs_url}) voor verdere informatie." + }, "step": { "user": { "description": "Weet u zeker dat u Dialogflow wilt instellen?", diff --git a/homeassistant/components/esphome/.translations/de.json b/homeassistant/components/esphome/.translations/de.json new file mode 100644 index 00000000000..191d930eb96 --- /dev/null +++ b/homeassistant/components/esphome/.translations/de.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "ESP ist bereits konfiguriert" + }, + "error": { + "connection_error": "Keine Verbindung zum ESP m\u00f6glich. Achten Sie darauf, dass Ihre YAML-Datei eine Zeile 'api:' enth\u00e4lt.", + "invalid_password": "Ung\u00fcltiges Passwort!", + "resolve_error": "Adresse des ESP kann nicht aufgel\u00f6st werden. Wenn dieser Fehler weiterhin besteht, legen Sie eine statische IP-Adresse fest: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" + }, + "step": { + "authenticate": { + "data": { + "password": "Passwort" + }, + "description": "Bitte geben Sie das Passwort der ESPhome-Konfiguration ein:", + "title": "Passwort eingeben" + }, + "user": { + "data": { + "host": "Host", + "port": "Port" + }, + "description": "Bitte geben Sie die Verbindungseinstellungen Ihres [ESPHome](https://esphomelib.com/)-Knotens ein.", + "title": "ESPHome" + } + }, + "title": "ESPHome" + } +} \ No newline at end of file diff --git a/homeassistant/components/esphome/.translations/fr.json b/homeassistant/components/esphome/.translations/fr.json new file mode 100644 index 00000000000..a021f1fe9f4 --- /dev/null +++ b/homeassistant/components/esphome/.translations/fr.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "invalid_password": "Mot de passe invalide !" + }, + "step": { + "authenticate": { + "data": { + "password": "Mot de passe" + }, + "title": "Entrer votre mot de passe" + }, + "user": { + "data": { + "host": "H\u00f4te", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/esphome/.translations/hu.json b/homeassistant/components/esphome/.translations/hu.json new file mode 100644 index 00000000000..7fe5da59de6 --- /dev/null +++ b/homeassistant/components/esphome/.translations/hu.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "invalid_password": "\u00c9rv\u00e9nytelen jelsz\u00f3!" + }, + "step": { + "authenticate": { + "data": { + "password": "Jelsz\u00f3" + }, + "title": "Adja meg a jelsz\u00f3t" + }, + "user": { + "data": { + "host": "Kiszolg\u00e1l\u00f3", + "port": "Port" + }, + "title": "ESPHome" + } + }, + "title": "ESPHome" + } +} \ No newline at end of file diff --git a/homeassistant/components/esphome/.translations/id.json b/homeassistant/components/esphome/.translations/id.json new file mode 100644 index 00000000000..837d18d27ad --- /dev/null +++ b/homeassistant/components/esphome/.translations/id.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "ESP sudah dikonfigurasi" + }, + "step": { + "authenticate": { + "data": { + "password": "Kata kunci" + }, + "description": "Silakan masukkan kata kunci yang Anda atur di konfigurasi Anda.", + "title": "Masukkan kata kunci" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/esphome/.translations/ko.json b/homeassistant/components/esphome/.translations/ko.json index dc1fcaf0bd1..514acbbbf18 100644 --- a/homeassistant/components/esphome/.translations/ko.json +++ b/homeassistant/components/esphome/.translations/ko.json @@ -13,7 +13,7 @@ "data": { "password": "\ube44\ubc00\ubc88\ud638" }, - "description": "ESP \uc5d0 \uad6c\uc131\ud55c \ube44\ubc00\ubc88\ud638\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694.", + "description": "ESP \uc5d0\uc11c \uc124\uc815\ud55c \ube44\ubc00\ubc88\ud638\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694.", "title": "\ube44\ubc00\ubc88\ud638 \uc785\ub825" }, "user": { diff --git a/homeassistant/components/esphome/.translations/nl.json b/homeassistant/components/esphome/.translations/nl.json new file mode 100644 index 00000000000..89831979d89 --- /dev/null +++ b/homeassistant/components/esphome/.translations/nl.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "ESP is al geconfigureerd" + }, + "error": { + "connection_error": "Kan geen verbinding maken met ESP. Zorg ervoor dat uw YAML-bestand een regel 'api:' bevat.", + "invalid_password": "Ongeldig wachtwoord!", + "resolve_error": "Kan het adres van de ESP niet vinden. Als deze fout aanhoudt, stel dan een statisch IP-adres in: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" + }, + "step": { + "authenticate": { + "data": { + "password": "Wachtwoord" + }, + "description": "Voer het wachtwoord in dat u in uw configuratie hebt ingesteld.", + "title": "Voer wachtwoord in" + }, + "user": { + "data": { + "host": "Host", + "port": "Poort" + }, + "description": "Voer de verbindingsinstellingen in van uw [ESPHome](https://esphomelib.com/) node.", + "title": "ESPHome" + } + }, + "title": "ESPHome" + } +} \ No newline at end of file diff --git a/homeassistant/components/esphome/.translations/no.json b/homeassistant/components/esphome/.translations/no.json new file mode 100644 index 00000000000..5f166eac74a --- /dev/null +++ b/homeassistant/components/esphome/.translations/no.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "ESP er allerede konfigurert" + }, + "error": { + "connection_error": "Kan ikke koble til ESP. Kontroller at YAML filen din inneholder en \"api:\" linje.", + "invalid_password": "Ugyldig passord!", + "resolve_error": "Kan ikke l\u00f8se adressen til ESP. Hvis denne feilen vedvarer, m\u00e5 du [angi en statisk IP-adresse](https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips)" + }, + "step": { + "authenticate": { + "data": { + "password": "Passord" + }, + "description": "Vennligst skriv inn passordet du har angitt i din konfigurasjon.", + "title": "Skriv Inn Passord" + }, + "user": { + "data": { + "host": "Vert", + "port": "Port" + }, + "description": "Vennligst skriv inn tilkoblingsinnstillinger for din [ESPHome](https://esphomelib.com/) node.", + "title": "ESPHome" + } + }, + "title": "ESPHome" + } +} \ No newline at end of file diff --git a/homeassistant/components/esphome/.translations/pl.json b/homeassistant/components/esphome/.translations/pl.json new file mode 100644 index 00000000000..4f2a8b0e1bb --- /dev/null +++ b/homeassistant/components/esphome/.translations/pl.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "ESP jest ju\u017c skonfigurowany" + }, + "error": { + "connection_error": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z ESP. Upewnij si\u0119, \u017ce Tw\u00f3j plik YAML zawiera lini\u0119 \"api:\".", + "invalid_password": "Nieprawid\u0142owe has\u0142o!", + "resolve_error": "Nie mo\u017cna rozpozna\u0107 adresu ESP. Je\u015bli ten b\u0142\u0105d b\u0119dzie si\u0119 powtarza\u0142, nale\u017cy ustawi\u0107 statyczny adres IP: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" + }, + "step": { + "authenticate": { + "data": { + "password": "Has\u0142o" + }, + "description": "Wprowad\u017a has\u0142o ustawione w konfiguracji.", + "title": "Wprowad\u017a has\u0142o" + }, + "user": { + "data": { + "host": "Host", + "port": "Port" + }, + "description": "Wprowad\u017a ustawienia po\u0142\u0105czenia swojego [ESPHome] (https://esphomelib.com/) w\u0119z\u0142a.", + "title": "ESPHome" + } + }, + "title": "ESPHome" + } +} \ No newline at end of file diff --git a/homeassistant/components/esphome/.translations/pt-BR.json b/homeassistant/components/esphome/.translations/pt-BR.json new file mode 100644 index 00000000000..87adc69021c --- /dev/null +++ b/homeassistant/components/esphome/.translations/pt-BR.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "O ESP j\u00e1 est\u00e1 configurado" + }, + "error": { + "connection_error": "N\u00e3o \u00e9 poss\u00edvel conectar-se ao ESP. Por favor, verifique se o seu arquivo YAML cont\u00e9m uma linha 'api:'.", + "invalid_password": "Senha inv\u00e1lida!", + "resolve_error": "N\u00e3o \u00e9 poss\u00edvel resolver o endere\u00e7o do ESP. Se este erro persistir, por favor, defina um endere\u00e7o IP est\u00e1tico: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" + }, + "step": { + "authenticate": { + "data": { + "password": "Senha" + }, + "description": "Por favor, digite a senha que voc\u00ea definiu em sua configura\u00e7\u00e3o.", + "title": "Digite a senha" + }, + "user": { + "data": { + "port": "Porta" + }, + "description": "Por favor insira as configura\u00e7\u00f5es de conex\u00e3o de seu n\u00f3 de [ESPHome] (https://esphomelib.com/).", + "title": "ESPHome" + } + }, + "title": "ESPHome" + } +} \ No newline at end of file diff --git a/homeassistant/components/esphome/.translations/pt.json b/homeassistant/components/esphome/.translations/pt.json new file mode 100644 index 00000000000..70e21e14666 --- /dev/null +++ b/homeassistant/components/esphome/.translations/pt.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "O ESP j\u00e1 est\u00e1 configurado" + }, + "error": { + "connection_error": "N\u00e3o \u00e9 poss\u00edvel conectar-se ao ESP. Por favor, verifique se o seu arquivo YAML cont\u00e9m uma linha 'api:'.", + "invalid_password": "Palavra-passe inv\u00e1lida", + "resolve_error": "N\u00e3o \u00e9 poss\u00edvel resolver o endere\u00e7o do ESP. Se este erro persistir, defina um endere\u00e7o IP est\u00e1tico: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" + }, + "step": { + "authenticate": { + "data": { + "password": "Palavra-passe" + }, + "description": "Por favor, insira a palavra-passe que colocou na configura\u00e7\u00e3o", + "title": "Palavra-passe" + }, + "user": { + "data": { + "host": "Servidor", + "port": "Porta" + }, + "description": "Por favor, insira as configura\u00e7\u00f5es de liga\u00e7\u00e3o ao seu n\u00f3 [ESPHome] (https://esphomelib.com/).", + "title": "" + } + }, + "title": "" + } +} \ No newline at end of file diff --git a/homeassistant/components/geofency/.translations/en.json b/homeassistant/components/geofency/.translations/en.json index e67af592c16..27b6335c6f9 100644 --- a/homeassistant/components/geofency/.translations/en.json +++ b/homeassistant/components/geofency/.translations/en.json @@ -1,18 +1,18 @@ { - "config": { - "title": "Geofency Webhook", - "step": { - "user": { - "title": "Set up the Geofency Webhook", - "description": "Are you sure you want to set up the Geofency Webhook?" - } - }, - "abort": { - "one_instance_allowed": "Only a single instance is necessary.", - "not_internet_accessible": "Your Home Assistant instance needs to be accessible from the internet to receive messages from Geofency." - }, - "create_entry": { - "default": "To send events to Home Assistant, you will need to setup the webhook feature in Geofency.\n\nFill in the following info:\n\n- URL: `{webhook_url}`\n- Method: POST\n\nSee [the documentation]({docs_url}) for further details." + "config": { + "abort": { + "not_internet_accessible": "Your Home Assistant instance needs to be accessible from the internet to receive messages from Geofency.", + "one_instance_allowed": "Only a single instance is necessary." + }, + "create_entry": { + "default": "To send events to Home Assistant, you will need to setup the webhook feature in Geofency.\n\nFill in the following info:\n\n- URL: `{webhook_url}`\n- Method: POST\n\nSee [the documentation]({docs_url}) for further details." + }, + "step": { + "user": { + "description": "Are you sure you want to set up the Geofency Webhook?", + "title": "Set up the Geofency Webhook" + } + }, + "title": "Geofency Webhook" } - } } \ No newline at end of file diff --git a/homeassistant/components/geofency/.translations/lb.json b/homeassistant/components/geofency/.translations/lb.json new file mode 100644 index 00000000000..490026b366d --- /dev/null +++ b/homeassistant/components/geofency/.translations/lb.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "\u00c4r Home Assistant Instanz muss iwwert Internet accessibel si fir Geofency Noriichten z'empf\u00e4nken.", + "one_instance_allowed": "N\u00ebmmen eng eenzeg Instanz ass n\u00e9ideg." + }, + "create_entry": { + "default": "Fir Evenementer un Home Assistant ze sch\u00e9cken, muss den Webhook Feature am Geofency ageriicht ginn.\n\nF\u00ebllt folgend Informatiounen aus:\n\n- URL: `{webhook_url}`\n- Method: POST\n\nLiest [Dokumentatioun]({docs_url}) fir w\u00e9ider D\u00e9tailer." + }, + "step": { + "user": { + "description": "S\u00e9cher fir Geofency Webhook anzeriichten?", + "title": "Geofency Webhook ariichten" + } + }, + "title": "Geofency Webhook" + } +} \ No newline at end of file diff --git a/homeassistant/components/hangouts/.translations/de.json b/homeassistant/components/hangouts/.translations/de.json index e0f18b6cccf..c8e84983fb6 100644 --- a/homeassistant/components/hangouts/.translations/de.json +++ b/homeassistant/components/hangouts/.translations/de.json @@ -5,7 +5,7 @@ "unknown": "Ein unbekannter Fehler ist aufgetreten." }, "error": { - "invalid_2fa": "Ung\u00fcltige 2-Faktor Authentifizierung, bitte versuche es erneut.", + "invalid_2fa": "Ung\u00fcltige 2-Faktor Authentifizierung, bitte versuchen Sie es erneut.", "invalid_2fa_method": "Ung\u00fcltige 2FA Methode (mit Telefon verifizieren)", "invalid_login": "Ung\u00fcltige Daten, bitte erneut versuchen." }, diff --git a/homeassistant/components/hangouts/.translations/pt-BR.json b/homeassistant/components/hangouts/.translations/pt-BR.json index 00c533311fc..444edc40838 100644 --- a/homeassistant/components/hangouts/.translations/pt-BR.json +++ b/homeassistant/components/hangouts/.translations/pt-BR.json @@ -11,6 +11,9 @@ }, "step": { "2fa": { + "data": { + "2fa": "Pin 2FA" + }, "description": "Vazio", "title": "Autentica\u00e7\u00e3o de 2 Fatores" }, diff --git a/homeassistant/components/hangouts/.translations/sl.json b/homeassistant/components/hangouts/.translations/sl.json index d7555335820..db85f338b67 100644 --- a/homeassistant/components/hangouts/.translations/sl.json +++ b/homeassistant/components/hangouts/.translations/sl.json @@ -6,7 +6,7 @@ }, "error": { "invalid_2fa": "Neveljavna 2FA avtorizacija, prosimo, poskusite znova.", - "invalid_2fa_method": "Neveljavna 2FA metoda (preveri na telefonu).", + "invalid_2fa_method": "Neveljavna 2FA Metoda (Preverite na Telefonu).", "invalid_login": "Neveljavna Prijava, prosimo, poskusite znova." }, "step": { @@ -15,7 +15,7 @@ "2fa": "2FA Pin" }, "description": "prazno", - "title": "2-faktorska avtorizacija" + "title": "Dvofaktorska avtorizacija" }, "user": { "data": { diff --git a/homeassistant/components/ifttt/.translations/pt-BR.json b/homeassistant/components/ifttt/.translations/pt-BR.json new file mode 100644 index 00000000000..4e72fc58b4b --- /dev/null +++ b/homeassistant/components/ifttt/.translations/pt-BR.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "description": "Tem certeza de que deseja configurar o IFTTT?", + "title": "Configurar o IFTTT Webhook Applet" + } + }, + "title": "IFTTT" + } +} \ No newline at end of file diff --git a/homeassistant/components/lifx/.translations/pt-BR.json b/homeassistant/components/lifx/.translations/pt-BR.json new file mode 100644 index 00000000000..e5f88b5384e --- /dev/null +++ b/homeassistant/components/lifx/.translations/pt-BR.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Nenhum dispositivo LIFX encontrado na rede.", + "single_instance_allowed": "Apenas uma configura\u00e7\u00e3o do LIFX \u00e9 poss\u00edvel." + }, + "step": { + "confirm": { + "description": "Voc\u00ea quer configurar o LIFX?", + "title": "LIFX" + } + }, + "title": "LIFX" + } +} \ No newline at end of file diff --git a/homeassistant/components/luftdaten/.translations/de.json b/homeassistant/components/luftdaten/.translations/de.json index 136b907df81..46d75a6b73b 100644 --- a/homeassistant/components/luftdaten/.translations/de.json +++ b/homeassistant/components/luftdaten/.translations/de.json @@ -1,15 +1,17 @@ { "config": { "error": { - "communication_error": "Keine Kommunikation mit Lufdaten API m\u00f6glich", + "communication_error": "Keine Kommunikation mit Luftdaten API m\u00f6glich", "invalid_sensor": "Sensor nicht verf\u00fcgbar oder ung\u00fcltig", "sensor_exists": "Sensor bereits registriert" }, "step": { "user": { "data": { - "show_on_map": "Auf Karte anzeigen" - } + "show_on_map": "Auf Karte anzeigen", + "station_id": "Luftdaten-Sensor-ID" + }, + "title": "Luftdaten einrichten" } }, "title": "Luftdaten" diff --git a/homeassistant/components/luftdaten/.translations/pt-BR.json b/homeassistant/components/luftdaten/.translations/pt-BR.json new file mode 100644 index 00000000000..796d7c04fbb --- /dev/null +++ b/homeassistant/components/luftdaten/.translations/pt-BR.json @@ -0,0 +1,14 @@ +{ + "config": { + "step": { + "user": { + "data": { + "show_on_map": "Mostrar no mapa", + "station_id": "ID do Sensor Luftdaten" + }, + "title": "Definir Luftdaten" + } + }, + "title": "Luftdaten" + } +} \ No newline at end of file diff --git a/homeassistant/components/luftdaten/.translations/pt.json b/homeassistant/components/luftdaten/.translations/pt.json index 6a242c441af..0402f352c5c 100644 --- a/homeassistant/components/luftdaten/.translations/pt.json +++ b/homeassistant/components/luftdaten/.translations/pt.json @@ -8,9 +8,12 @@ "step": { "user": { "data": { - "show_on_map": "Mostrar no mapa" - } + "show_on_map": "Mostrar no mapa", + "station_id": "Luftdaten Sensor ID" + }, + "title": "Definir Luftdaten" } - } + }, + "title": "" } } \ No newline at end of file diff --git a/homeassistant/components/mailgun/.translations/de.json b/homeassistant/components/mailgun/.translations/de.json new file mode 100644 index 00000000000..306757cd528 --- /dev/null +++ b/homeassistant/components/mailgun/.translations/de.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Ihre Home Assistant-Instanz muss \u00fcber das Internet erreichbar sein, um Mailgun-Nachrichten empfangen zu k\u00f6nnen.", + "one_instance_allowed": "Nur eine einzige Instanz ist notwendig." + }, + "create_entry": { + "default": "Um Ereignisse an den Home Assistant zu senden, m\u00fcssen Sie [Webhooks mit Mailgun]({mailgun_url}) einrichten. \n\n F\u00fcllen Sie die folgenden Informationen aus: \n\n - URL: ` {webhook_url} ` \n - Methode: POST \n - Inhaltstyp: application / json \n\n Lesen Sie in der [Dokumentation]({docs_url}) wie Sie Automationen f\u00fcr die Verarbeitung eingehender Daten konfigurieren." + }, + "step": { + "user": { + "description": "M\u00f6chten Sie Mailgun wirklich einrichten?", + "title": "Mailgun-Webhook einrichten" + } + }, + "title": "Mailgun" + } +} \ No newline at end of file diff --git a/homeassistant/components/mqtt/.translations/pt-BR.json b/homeassistant/components/mqtt/.translations/pt-BR.json index 2de8003e6ed..bc55b7d8c61 100644 --- a/homeassistant/components/mqtt/.translations/pt-BR.json +++ b/homeassistant/components/mqtt/.translations/pt-BR.json @@ -16,6 +16,12 @@ }, "description": "Por favor, insira as informa\u00e7\u00f5es de conex\u00e3o do seu agente MQTT.", "title": "MQTT" + }, + "hassio_confirm": { + "data": { + "discovery": "Ativar descoberta" + }, + "description": "Deseja configurar o Home Assistant para se conectar ao broker MQTT fornecido pelo complemento hass.io {addon}?" } }, "title": "MQTT" diff --git a/homeassistant/components/mqtt/.translations/uk.json b/homeassistant/components/mqtt/.translations/uk.json index e747e774c45..747d190a56d 100644 --- a/homeassistant/components/mqtt/.translations/uk.json +++ b/homeassistant/components/mqtt/.translations/uk.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "single_instance_allowed": "\u0414\u043e\u0437\u0432\u043e\u043b\u0435\u043d\u043e \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e MQTT." + }, "error": { "cannot_connect": "\u041d\u0435\u043c\u043e\u0436\u043b\u0438\u0432\u043e \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0438\u0441\u044f \u0434\u043e \u0431\u0440\u043e\u043a\u0435\u0440\u0430." }, @@ -11,7 +14,8 @@ "password": "\u041f\u0430\u0440\u043e\u043b\u044c", "port": "\u041f\u043e\u0440\u0442", "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" - } + }, + "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u044e \u043f\u0440\u043e \u0437'\u0454\u0434\u043d\u0430\u043d\u043d\u044f \u0432\u0430\u0448\u043e\u0433\u043e \u0431\u0440\u043e\u043a\u0435\u0440\u0430 MQTT." }, "hassio_confirm": { "data": { diff --git a/homeassistant/components/owntracks/.translations/de.json b/homeassistant/components/owntracks/.translations/de.json new file mode 100644 index 00000000000..fbd9cec2f5a --- /dev/null +++ b/homeassistant/components/owntracks/.translations/de.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "one_instance_allowed": "Nur eine einzige Instanz ist notwendig." + }, + "create_entry": { + "default": "\n\n\u00d6ffnen Sie unter Android [die OwnTracks-App]({android_url}) und gehen Sie zu {android_url} - > Verbindung. \u00c4ndern Sie die folgenden Einstellungen: \n - Modus: Privates HTTP \n - Host: {webhook_url} \n - Identifizierung: \n - Benutzername: ` ` \n - Ger\u00e4te-ID: ` ` \n\n\u00d6ffnen Sie unter iOS [die OwnTracks-App]({ios_url}) und tippen Sie auf das Symbol (i) oben links - > Einstellungen. \u00c4ndern Sie die folgenden Einstellungen: \n - Modus: HTTP \n - URL: {webhook_url} \n - Aktivieren Sie die Authentifizierung \n - UserID: ` ` \n\n {secret} \n \n Weitere Informationen finden Sie in der [Dokumentation]({docs_url})." + }, + "step": { + "user": { + "description": "M\u00f6chten Sie OwnTracks wirklich einrichten?", + "title": "OwnTracks einrichten" + } + }, + "title": "OwnTracks" + } +} \ No newline at end of file diff --git a/homeassistant/components/owntracks/.translations/nl.json b/homeassistant/components/owntracks/.translations/nl.json new file mode 100644 index 00000000000..21ee65a775a --- /dev/null +++ b/homeassistant/components/owntracks/.translations/nl.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "one_instance_allowed": "Slechts \u00e9\u00e9n instantie is nodig." + }, + "create_entry": { + "default": "\n\nOp Android, open [the OwnTracks app]({android_url}), ga naar 'preferences' -> 'connection'. Verander de volgende instellingen:\n - Mode: Private HTTP\n - Host: {webhook_url}\n - Identification:\n - Username: ``\n - Device ID: ``\n\nOp iOS, open [the OwnTracks app]({ios_url}), tik op het (i) icoon links boven -> 'settings'. Verander de volgende instellingen:\n - Mode: HTTP\n - URL: {webhook_url}\n - zet 'authentication' aan\n - UserID: ``\n\n{secret}\n\nZie [the documentation]({docs_url}) voor meer informatie." + }, + "step": { + "user": { + "description": "Weet je zeker dat je OwnTracks wilt instellen?", + "title": "Stel OwnTracks in" + } + }, + "title": "OwnTracks" + } +} \ No newline at end of file diff --git a/homeassistant/components/owntracks/.translations/uk.json b/homeassistant/components/owntracks/.translations/uk.json new file mode 100644 index 00000000000..8f4cdebbcd4 --- /dev/null +++ b/homeassistant/components/owntracks/.translations/uk.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "one_instance_allowed": "\u041f\u043e\u0442\u0440\u0456\u0431\u043d\u043e \u043b\u0438\u0448\u0435 \u043e\u0434\u0438\u043d \u0435\u043a\u0437\u0435\u043c\u043f\u043b\u044f\u0440." + }, + "step": { + "user": { + "description": "\u0412\u0438 \u0432\u043f\u0435\u0432\u043d\u0435\u043d\u0456, \u0449\u043e \u0445\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 OwnTracks?", + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f OwnTracks" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/point/.translations/de.json b/homeassistant/components/point/.translations/de.json new file mode 100644 index 00000000000..fe3b781bfac --- /dev/null +++ b/homeassistant/components/point/.translations/de.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_setup": "Sie k\u00f6nnen nur ein Point-Konto konfigurieren.", + "authorize_url_fail": "Unbekannter Fehler beim Erstellen der Authorisierungs-URL.", + "authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Authorisierungs-URL.", + "external_setup": "Pointt erfolgreich von einem anderen Flow konfiguriert.", + "no_flows": "Sie m\u00fcssen Point konfigurieren, bevor Sie sich damit authentifizieren k\u00f6nnen. [Bitte lesen Sie die Anweisungen] (https://www.home-assistant.io/components/point/)." + }, + "create_entry": { + "default": "Mit Minut erfolgreich f\u00fcr Ihre Point-Ger\u00e4te authentifiziert" + }, + "error": { + "follow_link": "Bitte folgen Sie dem Link und authentifizieren Sie sich, bevor Sie auf Senden klicken", + "no_token": "Nicht mit Minut authentifiziert" + }, + "step": { + "auth": { + "description": "Folgen Sie dem Link unten und Akzeptieren Zugriff auf Ihr Minut-Konto. Kehren Sie dann zur\u00fcck und dr\u00fccken Sie unten auf Senden . \n\n [Link]({authorization_url})", + "title": "Point authentifizieren" + }, + "user": { + "data": { + "flow_impl": "Anbieter" + }, + "description": "W\u00e4hlen Sie \u00fcber welchen Authentifizierungsanbieter Sie sich mit Point authentifizieren m\u00f6chten.", + "title": "Authentifizierungsanbieter" + } + }, + "title": "Minut Point" + } +} \ No newline at end of file diff --git a/homeassistant/components/point/.translations/nl.json b/homeassistant/components/point/.translations/nl.json index ff7f2cdcd56..50d9c7d45bb 100644 --- a/homeassistant/components/point/.translations/nl.json +++ b/homeassistant/components/point/.translations/nl.json @@ -1,12 +1,31 @@ { "config": { + "abort": { + "already_setup": "U kunt alleen een Point-account configureren.", + "authorize_url_fail": "Onbekende fout bij het genereren van een autoriseer-URL.", + "authorize_url_timeout": "Time-out tijdens genereren autorisatie url.", + "external_setup": "Punt succesvol geconfigureerd vanuit een andere stroom.", + "no_flows": "U moet Point configureren voordat u zich ermee kunt verifi\u00ebren. [Gelieve de instructies te lezen](https://www.home-assistant.io/components/nest/)." + }, + "create_entry": { + "default": "Succesvol geverifieerd met Minut voor uw Point appara(a)t(en)" + }, + "error": { + "follow_link": "Volg de link en verifieer voordat je op Verzenden klikt", + "no_token": "Niet geverifieerd met Minut" + }, "step": { + "auth": { + "description": "Ga naar onderstaande link en Accepteer toegang tot je Minut account, kom dan hier terug en klik op Verzenden hier onder.\n\n[Link]({authorization_url})", + "title": "Verificatie Point" + }, "user": { "data": { "flow_impl": "Leverancier" }, "title": "Authenticatieleverancier" } - } + }, + "title": "Minut Point" } } \ No newline at end of file diff --git a/homeassistant/components/point/.translations/pt-BR.json b/homeassistant/components/point/.translations/pt-BR.json new file mode 100644 index 00000000000..f6f281ec9f7 --- /dev/null +++ b/homeassistant/components/point/.translations/pt-BR.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_setup": "Voc\u00ea s\u00f3 pode configurar uma conta Point.", + "authorize_url_fail": "Erro desconhecido ao gerar URL de autoriza\u00e7\u00e3o.", + "authorize_url_timeout": "Excedido tempo limite gerando a URL de autoriza\u00e7\u00e3o.", + "external_setup": "Point configurado com \u00eaxito a partir de outro fluxo.", + "no_flows": "Voc\u00ea precisa configurar o Point antes de ser capaz de autenticar com ele. [Por favor, leia as instru\u00e7\u00f5es](https://www.home-assistant.io/components/point/)." + }, + "create_entry": { + "default": "Autenticado com sucesso com Minut para seu(s) dispositivo(s) Point" + }, + "error": { + "follow_link": "Por favor, siga o link e autentique antes de pressionar Enviar", + "no_token": "N\u00e3o autenticado com Minut" + }, + "step": { + "auth": { + "description": "Siga o link abaixo e Aceite o acesso \u00e0 sua conta Minut, depois volte e pressione Enviar. \n\n [Link]({authorization_url})" + }, + "user": { + "data": { + "flow_impl": "Provedor" + }, + "description": "Escolha atrav\u00e9s de qual provedor de autentica\u00e7\u00e3o voc\u00ea deseja autenticar com Point.", + "title": "Provedor de Autentica\u00e7\u00e3o" + } + }, + "title": "Minut Point" + } +} \ No newline at end of file diff --git a/homeassistant/components/point/.translations/pt.json b/homeassistant/components/point/.translations/pt.json index 8831696fcff..6d24d56233c 100644 --- a/homeassistant/components/point/.translations/pt.json +++ b/homeassistant/components/point/.translations/pt.json @@ -1,12 +1,32 @@ { "config": { + "abort": { + "already_setup": "S\u00f3 pode configurar uma \u00fanica conta Point.", + "authorize_url_fail": "Erro desconhecido ao gerar um URL de autoriza\u00e7\u00e3o.", + "authorize_url_timeout": "Limite temporal ultrapassado ao gerar um URL de autoriza\u00e7\u00e3o.", + "external_setup": "Point configurado com \u00eaxito a partir de outro fluxo.", + "no_flows": "\u00c9 necess\u00e1rio configurar o Point antes de poder autenticar com ele. [Por favor, leia as instru\u00e7\u00f5es] (https://www.home-assistant.io/components/point/)." + }, + "create_entry": { + "default": "Autenticado com sucesso com Minut para o(s) seu(s) dispositivo (s) Point" + }, "error": { - "follow_link": "Por favor, siga o link e autentique antes de pressionar Enviar" + "follow_link": "Por favor, siga o link e autentique antes de pressionar Enviar", + "no_token": "N\u00e3o autenticado com Minut" }, "step": { + "auth": { + "description": "Siga o link abaixo e Aceite o acesso \u00e0 sua conta Minut, depois volte e pressione Enviar abaixo. \n\n [Link] ( {authorization_url} )", + "title": "Autenticar Point" + }, "user": { + "data": { + "flow_impl": "Fornecedor" + }, + "description": "Escolha com qual fornecedor de autentica\u00e7\u00e3o deseja autenticar o Point.", "title": "Fornecedor de Autentica\u00e7\u00e3o" } - } + }, + "title": "" } } \ No newline at end of file diff --git a/homeassistant/components/rainmachine/.translations/hu.json b/homeassistant/components/rainmachine/.translations/hu.json index 2fbb55b2833..ff98eccbe5a 100644 --- a/homeassistant/components/rainmachine/.translations/hu.json +++ b/homeassistant/components/rainmachine/.translations/hu.json @@ -6,7 +6,9 @@ "step": { "user": { "data": { - "password": "Jelsz\u00f3" + "ip_address": "Kiszolg\u00e1l\u00f3 neve vagy IP c\u00edme", + "password": "Jelsz\u00f3", + "port": "Port" } } }, diff --git a/homeassistant/components/rainmachine/.translations/pt-BR.json b/homeassistant/components/rainmachine/.translations/pt-BR.json new file mode 100644 index 00000000000..8fdf05bd3c6 --- /dev/null +++ b/homeassistant/components/rainmachine/.translations/pt-BR.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "identifier_exists": "Conta j\u00e1 cadastrada", + "invalid_credentials": "Credenciais inv\u00e1lidas" + }, + "step": { + "user": { + "data": { + "ip_address": "Nome do host ou endere\u00e7o IP", + "password": "Senha", + "port": "Porta" + }, + "title": "Preencha suas informa\u00e7\u00f5es" + } + }, + "title": "RainMachine" + } +} \ No newline at end of file diff --git a/homeassistant/components/rainmachine/.translations/pt.json b/homeassistant/components/rainmachine/.translations/pt.json index 20f963d9dfb..97b9f84f26c 100644 --- a/homeassistant/components/rainmachine/.translations/pt.json +++ b/homeassistant/components/rainmachine/.translations/pt.json @@ -6,8 +6,14 @@ }, "step": { "user": { + "data": { + "ip_address": "Nome servidor ou endere\u00e7o IP", + "password": "Palavra-passe", + "port": "Porta" + }, "title": "Preencha as suas informa\u00e7\u00f5es" } - } + }, + "title": "" } } \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/moon.uk.json b/homeassistant/components/sensor/.translations/moon.uk.json new file mode 100644 index 00000000000..2467a705d50 --- /dev/null +++ b/homeassistant/components/sensor/.translations/moon.uk.json @@ -0,0 +1,12 @@ +{ + "state": { + "first_quarter": "\u041f\u0435\u0440\u0448\u0430 \u0447\u0432\u0435\u0440\u0442\u044c", + "full_moon": "\u041f\u043e\u0432\u043d\u0438\u0439 \u043c\u0456\u0441\u044f\u0446\u044c", + "last_quarter": "\u041e\u0441\u0442\u0430\u043d\u043d\u044f \u0447\u0432\u0435\u0440\u0442\u044c", + "new_moon": "\u041d\u043e\u0432\u0438\u0439 \u043c\u0456\u0441\u044f\u0446\u044c", + "waning_crescent": "\u0417\u0440\u043e\u0441\u0442\u0430\u044e\u0447\u0438\u0439 \u043f\u0456\u0432\u043c\u0456\u0441\u044f\u0446\u044c", + "waning_gibbous": "\u041c\u043e\u043b\u043e\u0434\u0438\u0439 \u043c\u0456\u0441\u044f\u0446\u044c", + "waxing_crescent": "\u041c\u043e\u043b\u043e\u0434\u0438\u0439 \u043c\u0456\u0441\u044f\u0446\u044c", + "waxing_gibbous": "\u041c\u043e\u043b\u043e\u0434\u0438\u0439 \u043c\u0456\u0441\u044f\u0446\u044c" + } +} \ No newline at end of file diff --git a/homeassistant/components/simplisafe/.translations/pt-BR.json b/homeassistant/components/simplisafe/.translations/pt-BR.json index 800ac719ca9..819cb1d95e0 100644 --- a/homeassistant/components/simplisafe/.translations/pt-BR.json +++ b/homeassistant/components/simplisafe/.translations/pt-BR.json @@ -7,11 +7,13 @@ "step": { "user": { "data": { + "code": "C\u00f3digo (para o Home Assistant)", "password": "Senha", "username": "Endere\u00e7o de e-mail" }, "title": "Preencha suas informa\u00e7\u00f5es" } - } + }, + "title": "SimpliSafe" } } \ No newline at end of file diff --git a/homeassistant/components/smhi/.translations/pt-BR.json b/homeassistant/components/smhi/.translations/pt-BR.json index 771195d8152..848f85ed133 100644 --- a/homeassistant/components/smhi/.translations/pt-BR.json +++ b/homeassistant/components/smhi/.translations/pt-BR.json @@ -1,7 +1,8 @@ { "config": { "error": { - "name_exists": "O nome j\u00e1 existe" + "name_exists": "O nome j\u00e1 existe", + "wrong_location": "Localiza\u00e7\u00e3o apenas na Su\u00e9cia" }, "step": { "user": { @@ -9,8 +10,10 @@ "latitude": "Latitude", "longitude": "Longitude", "name": "Nome" - } + }, + "title": "Localiza\u00e7\u00e3o na Su\u00e9cia" } - } + }, + "title": "Servi\u00e7o meteorol\u00f3gico sueco (SMHI)" } } \ No newline at end of file diff --git a/homeassistant/components/tellduslive/.translations/de.json b/homeassistant/components/tellduslive/.translations/de.json new file mode 100644 index 00000000000..4e9c32a1ee5 --- /dev/null +++ b/homeassistant/components/tellduslive/.translations/de.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "all_configured": "TelldusLive ist bereits konfiguriert", + "authorize_url_fail": "Unbekannter Fehler beim Erstellen der Authorisierungs-URL", + "authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Authorisierungs-URL.", + "unknown": "Unbekannter Fehler ist aufgetreten" + }, + "step": { + "auth": { + "description": "So verkn\u00fcpfen Sie Ihr TelldusLive-Konto: \n 1. Klicken Sie auf den Link unten \n 2. Melden Sie sich bei Telldus Live an \n 3. Autorisieren Sie ** {app_name} ** (klicken Sie auf ** Yes **). \n 4. Kommen Sie hierher zur\u00fcck und klicken Sie auf ** SUBMIT **. \n\n [Link TelldusLive-Konto]({auth_url})", + "title": "Authentifizieren Sie sich gegen TelldusLive" + }, + "user": { + "data": { + "host": "Host" + }, + "title": "Endpunkt ausw\u00e4hlen." + } + }, + "title": "Telldus Live" + } +} \ No newline at end of file diff --git a/homeassistant/components/tellduslive/.translations/fr.json b/homeassistant/components/tellduslive/.translations/fr.json new file mode 100644 index 00000000000..9a3121c5896 --- /dev/null +++ b/homeassistant/components/tellduslive/.translations/fr.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "user": { + "description": "Vide" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tellduslive/.translations/hu.json b/homeassistant/components/tellduslive/.translations/hu.json index 90ac7396f47..ffa983db093 100644 --- a/homeassistant/components/tellduslive/.translations/hu.json +++ b/homeassistant/components/tellduslive/.translations/hu.json @@ -2,6 +2,14 @@ "config": { "abort": { "unknown": "Ismeretlen hiba t\u00f6rt\u00e9nt" + }, + "step": { + "user": { + "data": { + "host": "Kiszolg\u00e1l\u00f3" + }, + "description": "\u00dcres" + } } } } \ No newline at end of file diff --git a/homeassistant/components/tellduslive/.translations/nl.json b/homeassistant/components/tellduslive/.translations/nl.json new file mode 100644 index 00000000000..609ac51c4de --- /dev/null +++ b/homeassistant/components/tellduslive/.translations/nl.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "all_configured": "TelldusLive is al geconfigureerd", + "authorize_url_fail": "Onbekende fout bij het genereren van een autorisatie url.", + "authorize_url_timeout": "Time-out tijdens genereren autorisatie url.", + "unknown": "Onbekende fout opgetreden" + }, + "step": { + "auth": { + "description": "Om uw TelldusLive-account te linken: \n 1. Klik op de onderstaande link \n 2. Log in op Telldus Live \n 3. Autoriseer ** {app_name} ** (klik op ** Ja **). \n 4. Kom hier terug en klik op ** VERSTUREN **. \n\n [Link TelldusLive account]({auth_url})", + "title": "Verifi\u00ebren tegen TelldusLive" + }, + "user": { + "data": { + "host": "Host" + }, + "description": "Leeg", + "title": "Kies eindpunt." + } + }, + "title": "Telldus Live" + } +} \ No newline at end of file diff --git a/homeassistant/components/tellduslive/.translations/pl.json b/homeassistant/components/tellduslive/.translations/pl.json index 63080a78632..5ee9ac221a7 100644 --- a/homeassistant/components/tellduslive/.translations/pl.json +++ b/homeassistant/components/tellduslive/.translations/pl.json @@ -15,6 +15,7 @@ "data": { "host": "Host" }, + "description": "Puste", "title": "Wybierz punkt ko\u0144cowy." } }, diff --git a/homeassistant/components/tellduslive/.translations/pt-BR.json b/homeassistant/components/tellduslive/.translations/pt-BR.json new file mode 100644 index 00000000000..c973aa223af --- /dev/null +++ b/homeassistant/components/tellduslive/.translations/pt-BR.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "unknown": "Ocorreu um erro desconhecido" + }, + "step": { + "auth": { + "description": "Para vincular sua conta do TelldusLive: \n 1. Clique no link abaixo \n 2. Fa\u00e7a o login no Telldus Live \n 3. Autorize **{app_name}** (clique em **Sim**). \n 4. Volte aqui e clique em **ENVIAR**. \n\n [Vincular conta TelldusLive]({auth_url})", + "title": "Autenticar no TelldusLive" + }, + "user": { + "title": "Escolha o ponto final." + } + }, + "title": "Telldus Live" + } +} \ No newline at end of file diff --git a/homeassistant/components/tellduslive/.translations/pt.json b/homeassistant/components/tellduslive/.translations/pt.json new file mode 100644 index 00000000000..d0d38a42caf --- /dev/null +++ b/homeassistant/components/tellduslive/.translations/pt.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "all_configured": "TelldusLive j\u00e1 est\u00e1 configurado", + "authorize_url_fail": "Erro desconhecido ao gerar um URL de autoriza\u00e7\u00e3o.", + "authorize_url_timeout": "Limite temporal ultrapassado ao gerar um URL de autoriza\u00e7\u00e3o.", + "unknown": "Ocorreu um erro desconhecido" + }, + "step": { + "auth": { + "description": "Para ligar \u00e0 sua conta do TelldusLive: \n 1. Clique no link abaixo \n 2. Fa\u00e7a o login no Telldus Live \n 3. Autorize **{app_name}** (clique em **Sim**). \n 4. Volte aqui e clique em **ENVIAR**. \n\n [Ligar \u00e0 TelldusLive] ( {auth_url} )", + "title": "Autenticar no TelldusLive" + }, + "user": { + "data": { + "host": "Servidor" + }, + "description": "Vazio", + "title": "Escolher endpoint." + } + }, + "title": "" + } +} \ No newline at end of file diff --git a/homeassistant/components/twilio/.translations/de.json b/homeassistant/components/twilio/.translations/de.json index 86e5d9051b3..91a195780fd 100644 --- a/homeassistant/components/twilio/.translations/de.json +++ b/homeassistant/components/twilio/.translations/de.json @@ -1,5 +1,18 @@ { "config": { + "abort": { + "not_internet_accessible": "Ihre Home Assistant-Instanz muss \u00fcber das Internet erreichbar sein, um Twilio-Nachrichten empfangen zu k\u00f6nnen.", + "one_instance_allowed": "Es ist nur eine einzige Instanz erforderlich." + }, + "create_entry": { + "default": "Um Ereignisse an den Home Assistant zu senden, m\u00fcssen Sie [Webhooks mit Twilio]({twilio_url}) einrichten. \n\n F\u00fcllen Sie die folgenden Informationen aus: \n\n - URL: ` {webhook_url} ` \n - Methode: POST \n - Inhaltstyp: application / x-www-form-urlencoded \n\nLesen Sie in der [Dokumentation]({docs_url}) wie Sie Automationen f\u00fcr die Verarbeitung eingehender Daten konfigurieren." + }, + "step": { + "user": { + "description": "M\u00f6chten Sie Twilio wirklich einrichten?", + "title": "Twilio-Webhook einrichten" + } + }, "title": "Twilio" } } \ No newline at end of file diff --git a/homeassistant/components/twilio/.translations/es.json b/homeassistant/components/twilio/.translations/es.json index 7927ce63e7f..6dbeff23c75 100644 --- a/homeassistant/components/twilio/.translations/es.json +++ b/homeassistant/components/twilio/.translations/es.json @@ -9,7 +9,8 @@ }, "step": { "user": { - "description": "\u00bfEst\u00e1s seguro de que quieres configurar Twilio?" + "description": "\u00bfEst\u00e1s seguro de que quieres configurar Twilio?", + "title": "Configurar el Webhook de Twilio" } }, "title": "Twilio" diff --git a/homeassistant/components/unifi/.translations/de.json b/homeassistant/components/unifi/.translations/de.json index 346c1937355..2b71d01417b 100644 --- a/homeassistant/components/unifi/.translations/de.json +++ b/homeassistant/components/unifi/.translations/de.json @@ -1,16 +1,26 @@ { "config": { "abort": { + "already_configured": "Controller-Site ist bereits konfiguriert", "user_privilege": "Der Benutzer muss Administrator sein" }, + "error": { + "faulty_credentials": "Ung\u00fcltige Anmeldeinformationen", + "service_unavailable": "Kein Dienst verf\u00fcgbar" + }, "step": { "user": { "data": { + "host": "Host", "password": "Passwort", "port": "Port", - "username": "Benutzername" - } + "site": "Site-ID", + "username": "Benutzername", + "verify_ssl": "Controller mit ordnungsgem\u00e4ssem Zertifikat" + }, + "title": "UniFi-Controller einrichten" } - } + }, + "title": "UniFi-Controller" } } \ No newline at end of file diff --git a/homeassistant/components/unifi/.translations/pt-BR.json b/homeassistant/components/unifi/.translations/pt-BR.json index 8b00dada642..d40dee22f24 100644 --- a/homeassistant/components/unifi/.translations/pt-BR.json +++ b/homeassistant/components/unifi/.translations/pt-BR.json @@ -1,8 +1,13 @@ { "config": { "abort": { + "already_configured": "O site de controle j\u00e1 est\u00e1 configurado", "user_privilege": "O usu\u00e1rio precisa ser administrador" }, + "error": { + "faulty_credentials": "Credenciais do usu\u00e1rio inv\u00e1lidas", + "service_unavailable": "Servi\u00e7o indispon\u00edvel" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/upnp/.translations/ca.json b/homeassistant/components/upnp/.translations/ca.json index 5f2606a448f..161b5d85599 100644 --- a/homeassistant/components/upnp/.translations/ca.json +++ b/homeassistant/components/upnp/.translations/ca.json @@ -4,9 +4,15 @@ "already_configured": "UPnP/IGD ja est\u00e0 configurat", "incomplete_device": "Ignorant el dispositiu incomplet UPnP", "no_devices_discovered": "No s'ha trobat cap UPnP/IGD", - "no_sensors_or_port_mapping": "Activa, com a m\u00ednim, els sensors o l'assignaci\u00f3 de ports" + "no_devices_found": "No s'han trobat dispositius UPnP/IGD a la xarxa.", + "no_sensors_or_port_mapping": "Activa, com a m\u00ednim, els sensors o l'assignaci\u00f3 de ports", + "single_instance_allowed": "Nom\u00e9s cal una sola configuraci\u00f3 de UPnP/IGD." }, "step": { + "confirm": { + "description": "Vols configurar UPnP/IGD?", + "title": "UPnP/IGD" + }, "init": { "title": "UPnP/IGD" }, diff --git a/homeassistant/components/upnp/.translations/de.json b/homeassistant/components/upnp/.translations/de.json index 675d1eb7d0c..51faf56367d 100644 --- a/homeassistant/components/upnp/.translations/de.json +++ b/homeassistant/components/upnp/.translations/de.json @@ -2,10 +2,17 @@ "config": { "abort": { "already_configured": "UPnP/IGD ist bereits konfiguriert", + "incomplete_device": "Unvollst\u00e4ndiges UPnP-Ger\u00e4t wird ignoriert", "no_devices_discovered": "Keine UPnP/IGDs entdeckt", - "no_sensors_or_port_mapping": "Aktiviere mindestens Sensoren oder Port-Mapping" + "no_devices_found": "Keine UPnP/IGD-Ger\u00e4te im Netzwerk gefunden.", + "no_sensors_or_port_mapping": "Aktiviere mindestens Sensoren oder Port-Mapping", + "single_instance_allowed": "Es ist nur eine einzige Konfiguration von UPnP/IGD erforderlich." }, "step": { + "confirm": { + "description": "M\u00f6chten Sie UPnP/IGD einrichten?", + "title": "UPnP/IGD" + }, "init": { "title": "UPnP/IGD" }, diff --git a/homeassistant/components/upnp/.translations/en.json b/homeassistant/components/upnp/.translations/en.json index 76384beac71..632d5112f1a 100644 --- a/homeassistant/components/upnp/.translations/en.json +++ b/homeassistant/components/upnp/.translations/en.json @@ -4,9 +4,15 @@ "already_configured": "UPnP/IGD is already configured", "incomplete_device": "Ignoring incomplete UPnP device", "no_devices_discovered": "No UPnP/IGDs discovered", - "no_sensors_or_port_mapping": "Enable at least sensors or port mapping" + "no_devices_found": "No UPnP/IGD devices found on the network.", + "no_sensors_or_port_mapping": "Enable at least sensors or port mapping", + "single_instance_allowed": "Only a single configuration of UPnP/IGD is necessary." }, "step": { + "confirm": { + "description": "Do you want to set up UPnP/IGD?", + "title": "UPnP/IGD" + }, "init": { "title": "UPnP/IGD" }, diff --git a/homeassistant/components/upnp/.translations/es.json b/homeassistant/components/upnp/.translations/es.json index 652ff87d9d4..6afdeca8047 100644 --- a/homeassistant/components/upnp/.translations/es.json +++ b/homeassistant/components/upnp/.translations/es.json @@ -6,6 +6,10 @@ "no_devices_discovered": "No se descubrieron UPnP / IGDs", "no_sensors_or_port_mapping": "Habilitar al menos sensores o mapeo de puertos" }, + "error": { + "one": "UNO", + "other": "OTRO" + }, "step": { "init": { "title": "UPnP / IGD" diff --git a/homeassistant/components/upnp/.translations/hu.json b/homeassistant/components/upnp/.translations/hu.json index 466c80f9e56..f2fd380b1e3 100644 --- a/homeassistant/components/upnp/.translations/hu.json +++ b/homeassistant/components/upnp/.translations/hu.json @@ -1,10 +1,17 @@ { "config": { + "abort": { + "no_devices_found": "Nincsenek UPnPIGD eszk\u00f6z\u00f6k a h\u00e1l\u00f3zaton." + }, "error": { "one": "hiba", "other": "" }, "step": { + "confirm": { + "description": "Be akarja \u00e1ll\u00edtani a UPnP/IGD-t?", + "title": "UPnP/IGD" + }, "init": { "title": "UPnP/IGD" }, diff --git a/homeassistant/components/upnp/.translations/ko.json b/homeassistant/components/upnp/.translations/ko.json index 9e10ae1d67c..d38d5be58ba 100644 --- a/homeassistant/components/upnp/.translations/ko.json +++ b/homeassistant/components/upnp/.translations/ko.json @@ -4,12 +4,18 @@ "already_configured": "UPnP/IGD \uac00 \uc774\ubbf8 \uc124\uc815\ub41c \uc0c1\ud0dc\uc785\ub2c8\ub2e4", "incomplete_device": "\ubd88\uc644\uc804\ud55c UPnP \uc7a5\uce58 \ubb34\uc2dc\ud558\uae30", "no_devices_discovered": "\ubc1c\uacac\ub41c UPnP/IGD \uac00 \uc5c6\uc2b5\ub2c8\ub2e4", - "no_sensors_or_port_mapping": "\ucd5c\uc18c\ud55c \uc13c\uc11c \ud639\uc740 \ud3ec\ud2b8 \ub9e4\ud551\uc744 \ud65c\uc131\ud654 \ud574\uc57c \ud569\ub2c8\ub2e4" + "no_devices_found": "UPnP/IGD \uc7a5\uce58\uac00 \ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \ubc1c\uacac\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4.", + "no_sensors_or_port_mapping": "\ucd5c\uc18c\ud55c \uc13c\uc11c \ud639\uc740 \ud3ec\ud2b8 \ub9e4\ud551\uc744 \ud65c\uc131\ud654 \ud574\uc57c \ud569\ub2c8\ub2e4", + "single_instance_allowed": "\ud558\ub098\uc758 UPnP/IGD \ub9cc \uad6c\uc131 \ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4." }, "error": { "other": "\ub2e4\ub978" }, "step": { + "confirm": { + "description": "UPnP/IGD \ub97c \uc124\uc815 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "title": "UPnP/IGD" + }, "init": { "title": "UPnP/IGD" }, diff --git a/homeassistant/components/upnp/.translations/lb.json b/homeassistant/components/upnp/.translations/lb.json index 183144afb53..029e1e87cf1 100644 --- a/homeassistant/components/upnp/.translations/lb.json +++ b/homeassistant/components/upnp/.translations/lb.json @@ -4,13 +4,19 @@ "already_configured": "UPnP/IGD ass scho konfigur\u00e9iert", "incomplete_device": "Ignor\u00e9iert onvollst\u00e4nnegen UPnP-Apparat", "no_devices_discovered": "Keng UPnP/IGDs entdeckt", - "no_sensors_or_port_mapping": "Aktiv\u00e9ier op mannst Sensoren oder Port Mapping" + "no_devices_found": "Keng UPnP/IGD Apparater am Netzwierk fonnt.", + "no_sensors_or_port_mapping": "Aktiv\u00e9ier op mannst Sensoren oder Port Mapping", + "single_instance_allowed": "N\u00ebmmen eng eenzeg Konfiguratioun vun UPnP/IGD ass n\u00e9ideg." }, "error": { "one": "Een", "other": "Aaner" }, "step": { + "confirm": { + "description": "Soll UPnP/IGD konfigur\u00e9iert ginn?", + "title": "UPnP/IGD" + }, "init": { "title": "UPnP/IGD" }, diff --git a/homeassistant/components/upnp/.translations/nl.json b/homeassistant/components/upnp/.translations/nl.json index c6939f9a0a7..5d426f2edaf 100644 --- a/homeassistant/components/upnp/.translations/nl.json +++ b/homeassistant/components/upnp/.translations/nl.json @@ -1,12 +1,18 @@ { "config": { "abort": { - "already_configured": "UPnP / IGD is al geconfigureerd", + "already_configured": "UPnP/IGD is al geconfigureerd", "incomplete_device": "Onvolledig UPnP-apparaat negeren", - "no_devices_discovered": "Geen UPnP / IGD's ontdekt", - "no_sensors_or_port_mapping": "Schakel ten minste sensoren of poorttoewijzing in" + "no_devices_discovered": "Geen UPnP'/IGD's ontdekt", + "no_devices_found": "Geen UPnP/IGD apparaten gevonden op het netwerk.", + "no_sensors_or_port_mapping": "Schakel ten minste sensoren of poorttoewijzing in", + "single_instance_allowed": "Er is slechts \u00e9\u00e9n configuratie van UPnP/IGD nodig." }, "step": { + "confirm": { + "description": "Wilt u UPnP/IGD instellen?", + "title": "UPnP/IGD" + }, "init": { "title": "UPnP/IGD" }, @@ -19,6 +25,6 @@ "title": "Configuratiemogelijkheden voor de UPnP/IGD" } }, - "title": "UPnP / IGD" + "title": "UPnP/IGD" } } \ No newline at end of file diff --git a/homeassistant/components/upnp/.translations/no.json b/homeassistant/components/upnp/.translations/no.json index 50b661627e3..813509121e3 100644 --- a/homeassistant/components/upnp/.translations/no.json +++ b/homeassistant/components/upnp/.translations/no.json @@ -4,7 +4,9 @@ "already_configured": "UPnP / IGD er allerede konfigurert", "incomplete_device": "Ignorerer ufullstendig UPnP-enhet", "no_devices_discovered": "Ingen UPnP / IGDs oppdaget", - "no_sensors_or_port_mapping": "Aktiver minst sensorer eller port mapping" + "no_devices_found": "Ingen UPnP / IGD-enheter funnet p\u00e5 nettverket.", + "no_sensors_or_port_mapping": "Aktiver minst sensorer eller port mapping", + "single_instance_allowed": "Bare en enkelt konfigurasjon av UPnP / IGD er n\u00f8dvendig." }, "error": { "few": "f\u00e5", @@ -15,6 +17,10 @@ "zero": "ingen" }, "step": { + "confirm": { + "description": "\u00d8nsker du \u00e5 konfigurere UPnP / IGD?", + "title": "UPnP / IGD" + }, "init": { "title": "UPnP / IGD" }, diff --git a/homeassistant/components/upnp/.translations/pl.json b/homeassistant/components/upnp/.translations/pl.json index d01946cb6e2..d7ede44d22d 100644 --- a/homeassistant/components/upnp/.translations/pl.json +++ b/homeassistant/components/upnp/.translations/pl.json @@ -4,7 +4,9 @@ "already_configured": "UPnP/IGD jest ju\u017c skonfigurowane", "incomplete_device": "Ignorowanie niekompletnego urz\u0105dzenia UPnP", "no_devices_discovered": "Nie wykryto urz\u0105dze\u0144 UPnP/IGD", - "no_sensors_or_port_mapping": "W\u0142\u0105cz przynajmniej sensory lub mapowanie port\u00f3w" + "no_devices_found": "Nie znaleziono w sieci urz\u0105dze\u0144 UPnP/IGD.", + "no_sensors_or_port_mapping": "W\u0142\u0105cz przynajmniej sensory lub mapowanie port\u00f3w", + "single_instance_allowed": "Wymagana jest tylko jedna konfiguracja UPnP/IGD." }, "error": { "few": "kilka", @@ -13,6 +15,10 @@ "other": "inne" }, "step": { + "confirm": { + "description": "Czy chcesz skonfigurowa\u0107 UPnP/IGD?", + "title": "UPnP/IGD" + }, "init": { "title": "UPnP/IGD" }, diff --git a/homeassistant/components/upnp/.translations/pt-BR.json b/homeassistant/components/upnp/.translations/pt-BR.json new file mode 100644 index 00000000000..4dd71176cf4 --- /dev/null +++ b/homeassistant/components/upnp/.translations/pt-BR.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "no_devices_discovered": "Nenhum UPnP/IGD descoberto", + "no_devices_found": "Nenhum dispositivo UPnP/IGD encontrado na rede.", + "no_sensors_or_port_mapping": "Ative pelo menos sensores ou mapeamento de porta", + "single_instance_allowed": "Apenas uma configura\u00e7\u00e3o do UPnP/IGD \u00e9 necess\u00e1ria." + }, + "step": { + "confirm": { + "description": "Deseja configurar o UPnP/IGD?", + "title": "UPnP/IGD" + }, + "init": { + "title": "UPnP/IGD" + }, + "user": { + "data": { + "enable_port_mapping": "Ativar o mapeamento de porta para o Home Assistant", + "enable_sensors": "Adicionar sensores de tr\u00e1fego", + "igd": "UPnP/IGD" + }, + "title": "Op\u00e7\u00f5es de configura\u00e7\u00e3o para o UPnP/IGD" + } + }, + "title": "UPnP/IGD" + } +} \ No newline at end of file diff --git a/homeassistant/components/upnp/.translations/pt.json b/homeassistant/components/upnp/.translations/pt.json index 99d056a7d78..5c5693e6a0c 100644 --- a/homeassistant/components/upnp/.translations/pt.json +++ b/homeassistant/components/upnp/.translations/pt.json @@ -4,13 +4,19 @@ "already_configured": "UPnP/IGD j\u00e1 est\u00e1 configurado", "incomplete_device": "Dispositivos UPnP incompletos ignorados", "no_devices_discovered": "Nenhum UPnP/IGDs descoberto", - "no_sensors_or_port_mapping": "Ative pelo menos os sensores ou o mapeamento de porta" + "no_devices_found": "Nenhum dispositivo UPnP / IGD encontrado na rede.", + "no_sensors_or_port_mapping": "Ative pelo menos os sensores ou o mapeamento de porta", + "single_instance_allowed": "Apenas uma \u00fanica configura\u00e7\u00e3o do UPnP/IGD \u00e9 necess\u00e1ria." }, "error": { "one": "um", "other": "v\u00e1rios" }, "step": { + "confirm": { + "description": "Deseja configurar o UPnP / IGD?", + "title": "" + }, "init": { "title": "UPnP/IGD" }, diff --git a/homeassistant/components/upnp/.translations/ru.json b/homeassistant/components/upnp/.translations/ru.json index 8e86c41366b..6a7c43f9e46 100644 --- a/homeassistant/components/upnp/.translations/ru.json +++ b/homeassistant/components/upnp/.translations/ru.json @@ -4,9 +4,15 @@ "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", "incomplete_device": "\u0418\u0433\u043d\u043e\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u043d\u0435\u043f\u043e\u043b\u043d\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 UPnP", "no_devices_discovered": "\u041d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u043e UPnP / IGD", - "no_sensors_or_port_mapping": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0434\u0430\u0442\u0447\u0438\u043a\u0438 \u0438\u043b\u0438 \u043f\u0440\u0435\u043e\u0431\u0440\u0430\u0437\u043e\u0432\u0430\u043d\u0438\u0435 \u043f\u043e\u0440\u0442\u043e\u0432 " + "no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 UPnP / IGD \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b \u0432 \u0441\u0435\u0442\u0438.", + "no_sensors_or_port_mapping": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0434\u0430\u0442\u0447\u0438\u043a\u0438 \u0438\u043b\u0438 \u043f\u0440\u0435\u043e\u0431\u0440\u0430\u0437\u043e\u0432\u0430\u043d\u0438\u0435 \u043f\u043e\u0440\u0442\u043e\u0432 ", + "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." }, "step": { + "confirm": { + "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c UPnP / IGD?", + "title": "UPnP / IGD" + }, "init": { "title": "UPnP / IGD" }, diff --git a/homeassistant/components/upnp/.translations/sl.json b/homeassistant/components/upnp/.translations/sl.json index f7052051192..4bf6501bd2a 100644 --- a/homeassistant/components/upnp/.translations/sl.json +++ b/homeassistant/components/upnp/.translations/sl.json @@ -4,7 +4,9 @@ "already_configured": "UPnP/IGD je \u017ee konfiguriran", "incomplete_device": "Ignoriranje nepopolnih UPnP naprav", "no_devices_discovered": "Ni odkritih UPnP/IGD naprav", - "no_sensors_or_port_mapping": "Omogo\u010dite vsaj senzorje ali preslikavo vrat (port mapping)" + "no_devices_found": "Naprav UPnP/IGD ni mogo\u010de najti v omre\u017eju.", + "no_sensors_or_port_mapping": "Omogo\u010dite vsaj senzorje ali preslikavo vrat (port mapping)", + "single_instance_allowed": "Potrebna je samo ena konfiguracija UPnp/IGD." }, "error": { "few": "nekaj", @@ -13,6 +15,10 @@ "two": "dve" }, "step": { + "confirm": { + "description": "Ali \u017eelite nastaviti UPnp/IGD?", + "title": "UPnP/IGD" + }, "init": { "title": "UPnP/IGD" }, diff --git a/homeassistant/components/upnp/.translations/uk.json b/homeassistant/components/upnp/.translations/uk.json new file mode 100644 index 00000000000..44268a5b5b5 --- /dev/null +++ b/homeassistant/components/upnp/.translations/uk.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "UPnP/IGD \u0432\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0454\u043d\u043e", + "no_devices_discovered": "\u041d\u0435 \u0432\u0438\u044f\u0432\u043b\u0435\u043d\u043e UPnP/IGD", + "no_sensors_or_port_mapping": "\u0423\u0432\u0456\u043c\u043a\u043d\u0456\u0442\u044c \u043f\u0440\u0438\u043d\u0430\u0439\u043c\u043d\u0456 \u0434\u0430\u0442\u0447\u0438\u043a\u0438 \u0430\u0431\u043e \u0432\u0456\u0434\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u043d\u044f \u043f\u043e\u0440\u0442\u0456\u0432" + }, + "step": { + "user": { + "data": { + "enable_port_mapping": "\u0423\u0432\u0456\u043c\u043a\u043d\u0443\u0442\u0438 \u0432\u0456\u0434\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u043d\u044f \u043f\u043e\u0440\u0442\u0456\u0432 \u0434\u043b\u044f Home Assistant", + "enable_sensors": "\u0414\u043e\u0434\u0430\u0442\u0438 \u0434\u0430\u0442\u0447\u0438\u043a\u0438 \u0442\u0440\u0430\u0444\u0456\u043a\u0443" + }, + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0457\u0442\u0438 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0438 UPnP/IGD" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/upnp/.translations/zh-Hant.json b/homeassistant/components/upnp/.translations/zh-Hant.json index 2c1fe82c523..2a036a1d2f3 100644 --- a/homeassistant/components/upnp/.translations/zh-Hant.json +++ b/homeassistant/components/upnp/.translations/zh-Hant.json @@ -4,9 +4,15 @@ "already_configured": "UPnP/IGD \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "incomplete_device": "\u5ffd\u7565\u4e0d\u76f8\u5bb9 UPnP \u88dd\u7f6e", "no_devices_discovered": "\u672a\u641c\u5c0b\u5230 UPnP/IGD", - "no_sensors_or_port_mapping": "\u81f3\u5c11\u958b\u555f\u611f\u61c9\u5668\u6216\u901a\u8a0a\u57e0\u8f49\u767c" + "no_devices_found": "\u5728\u7db2\u8def\u4e0a\u627e\u4e0d\u5230 UPnP/IGD \u88dd\u7f6e\u3002", + "no_sensors_or_port_mapping": "\u81f3\u5c11\u958b\u555f\u611f\u61c9\u5668\u6216\u901a\u8a0a\u57e0\u8f49\u767c", + "single_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u6b21 UPnP/IGD \u5373\u53ef\u3002" }, "step": { + "confirm": { + "description": "\u662f\u5426\u8981\u8a2d\u5b9a UPnP/IGD\uff1f", + "title": "UPnP/IGD" + }, "init": { "title": "UPnP/IGD" }, diff --git a/homeassistant/components/zha/.translations/de.json b/homeassistant/components/zha/.translations/de.json new file mode 100644 index 00000000000..280c941b427 --- /dev/null +++ b/homeassistant/components/zha/.translations/de.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Es ist nur eine einzige Konfiguration von ZHA zul\u00e4ssig." + }, + "error": { + "cannot_connect": "Kein Verbindung zu ZHA-Ger\u00e4t m\u00f6glich" + }, + "step": { + "user": { + "data": { + "radio_type": "Radio-Type", + "usb_path": "USB-Ger\u00e4te-Pfad" + }, + "title": "ZHA" + } + }, + "title": "ZHA" + } +} \ No newline at end of file diff --git a/homeassistant/components/zha/.translations/nl.json b/homeassistant/components/zha/.translations/nl.json index e7a3c901c21..b2af24aceac 100644 --- a/homeassistant/components/zha/.translations/nl.json +++ b/homeassistant/components/zha/.translations/nl.json @@ -1,5 +1,11 @@ { "config": { + "abort": { + "single_instance_allowed": "Slechts \u00e9\u00e9n configuratie van ZHA is toegestaan." + }, + "error": { + "cannot_connect": "Kan geen verbinding maken met ZHA apparaat." + }, "step": { "user": { "data": { diff --git a/homeassistant/components/zha/.translations/pt-BR.json b/homeassistant/components/zha/.translations/pt-BR.json new file mode 100644 index 00000000000..c8eb87a5181 --- /dev/null +++ b/homeassistant/components/zha/.translations/pt-BR.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Apenas uma configura\u00e7\u00e3o do ZHA \u00e9 permitida." + }, + "error": { + "cannot_connect": "N\u00e3o \u00e9 poss\u00edvel conectar-se ao dispositivo ZHA." + }, + "step": { + "user": { + "data": { + "radio_type": "Tipo de r\u00e1dio", + "usb_path": "Caminho do Dispositivo USB" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zha/.translations/pt.json b/homeassistant/components/zha/.translations/pt.json index 8db9f20dc7b..0259403fabc 100644 --- a/homeassistant/components/zha/.translations/pt.json +++ b/homeassistant/components/zha/.translations/pt.json @@ -12,8 +12,10 @@ "radio_type": "Tipo de r\u00e1dio", "usb_path": "Caminho do Dispositivo USB" }, - "description": "Vazio" + "description": "Vazio", + "title": "" } - } + }, + "title": "" } } \ No newline at end of file diff --git a/homeassistant/components/zwave/.translations/pt-BR.json b/homeassistant/components/zwave/.translations/pt-BR.json index 16c25cb7cab..2b4b19cde5a 100644 --- a/homeassistant/components/zwave/.translations/pt-BR.json +++ b/homeassistant/components/zwave/.translations/pt-BR.json @@ -1,5 +1,22 @@ { "config": { + "abort": { + "already_configured": "Z-Wave j\u00e1 est\u00e1 configurado.", + "one_instance_only": "Componente suporta apenas uma inst\u00e2ncia do Z-Wave" + }, + "error": { + "option_error": "A valida\u00e7\u00e3o Z-Wave falhou. O caminho para o USB est\u00e1 correto?" + }, + "step": { + "user": { + "data": { + "network_key": "Chave de rede (deixe em branco para gerar automaticamente)", + "usb_path": "Caminho do USB" + }, + "description": "Consulte https://www.home-assistant.io/docs/z-wave/installation/ para obter informa\u00e7\u00f5es sobre as vari\u00e1veis de configura\u00e7\u00e3o", + "title": "Configurar o Z-Wave" + } + }, "title": "Z-Wave" } } \ No newline at end of file From 01eee52990f0dc27f90ff3138e5fbe0c94bd2f96 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Wed, 9 Jan 2019 19:38:19 -0700 Subject: [PATCH 070/235] Bump pyflunearyou to 1.0.1 (#19899) --- homeassistant/components/sensor/flunearyou.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/flunearyou.py b/homeassistant/components/sensor/flunearyou.py index ee1c378aea3..a1a306f36e0 100644 --- a/homeassistant/components/sensor/flunearyou.py +++ b/homeassistant/components/sensor/flunearyou.py @@ -18,7 +18,7 @@ from homeassistant.helpers import aiohttp_client from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle -REQUIREMENTS = ['pyflunearyou==1.0.0'] +REQUIREMENTS = ['pyflunearyou==1.0.1'] _LOGGER = logging.getLogger(__name__) ATTR_CITY = 'city' diff --git a/requirements_all.txt b/requirements_all.txt index 8f4a916d547..cadecc54eb4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -988,7 +988,7 @@ pyflexit==0.3 pyflic-homeassistant==0.4.dev0 # homeassistant.components.sensor.flunearyou -pyflunearyou==1.0.0 +pyflunearyou==1.0.1 # homeassistant.components.light.futurenow pyfnip==0.2 From 8b49ecbe7d4074ea8f8185fbe0a45792217ab686 Mon Sep 17 00:00:00 2001 From: Artem Tokarev Date: Thu, 10 Jan 2019 08:18:59 +0200 Subject: [PATCH 071/235] =?UTF-8?q?Removed=20mkdir,=20If=20the=20WORKDIR?= =?UTF-8?q?=20doesn=E2=80=99t=20exist,=20it=20will=20be=20created.=20(#198?= =?UTF-8?q?92)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 1 - virtualization/Docker/Dockerfile.dev | 1 - 2 files changed, 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 4cd4f3e8871..0dcd0f666c7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -16,7 +16,6 @@ LABEL maintainer="Paulus Schoutsen " VOLUME /config -RUN mkdir -p /usr/src/app WORKDIR /usr/src/app # Copy build scripts diff --git a/virtualization/Docker/Dockerfile.dev b/virtualization/Docker/Dockerfile.dev index 3046fe51ba3..41f447dff12 100644 --- a/virtualization/Docker/Dockerfile.dev +++ b/virtualization/Docker/Dockerfile.dev @@ -17,7 +17,6 @@ LABEL maintainer="Paulus Schoutsen " VOLUME /config -RUN mkdir -p /usr/src/app WORKDIR /usr/src/app # Copy build scripts From e6a2c184303512df3103202bc5582fafecc99a55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Wiedemann?= Date: Thu, 10 Jan 2019 13:48:24 +0100 Subject: [PATCH 072/235] min_max sensor support for STATE_UNAVAILABLE --- homeassistant/components/sensor/min_max.py | 6 ++++-- tests/components/sensor/test_min_max.py | 5 +++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/sensor/min_max.py b/homeassistant/components/sensor/min_max.py index 7d9e91a1bf1..f0334ef3255 100644 --- a/homeassistant/components/sensor/min_max.py +++ b/homeassistant/components/sensor/min_max.py @@ -11,7 +11,8 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_NAME, STATE_UNKNOWN, CONF_TYPE, ATTR_UNIT_OF_MEASUREMENT) + CONF_NAME, STATE_UNKNOWN, STATE_UNAVAILABLE, CONF_TYPE, + ATTR_UNIT_OF_MEASUREMENT) from homeassistant.core import callback from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_state_change @@ -125,7 +126,8 @@ class MinMaxSensor(Entity): @callback def async_min_max_sensor_state_listener(entity, old_state, new_state): """Handle the sensor state changes.""" - if new_state.state is None or new_state.state in STATE_UNKNOWN: + if (new_state.state is None + or new_state.state in [STATE_UNKNOWN, STATE_UNAVAILABLE]): self.states[entity] = STATE_UNKNOWN hass.async_add_job(self.async_update_ha_state, True) return diff --git a/tests/components/sensor/test_min_max.py b/tests/components/sensor/test_min_max.py index ae2f40e5802..f76a05c2ce0 100644 --- a/tests/components/sensor/test_min_max.py +++ b/tests/components/sensor/test_min_max.py @@ -3,7 +3,8 @@ import unittest from homeassistant.setup import setup_component from homeassistant.const import ( - STATE_UNKNOWN, ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS, TEMP_FAHRENHEIT) + STATE_UNKNOWN, STATE_UNAVAILABLE, ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS, + TEMP_FAHRENHEIT) from tests.common import get_test_home_assistant @@ -209,7 +210,7 @@ class TestMinMaxSensor(unittest.TestCase): state = self.hass.states.get('sensor.test_max') assert STATE_UNKNOWN != state.state - self.hass.states.set(entity_ids[1], STATE_UNKNOWN) + self.hass.states.set(entity_ids[1], STATE_UNAVAILABLE) self.hass.block_till_done() state = self.hass.states.get('sensor.test_max') From c44f5d31ef42a6a1ee390f7307153e03e709fc0d Mon Sep 17 00:00:00 2001 From: Roy Duineveld Date: Thu, 10 Jan 2019 15:49:13 +0100 Subject: [PATCH 073/235] Plant monitor defaults (#19891) * Plant monitor defaults * houndci-bot fixes --- homeassistant/components/plant.py | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/plant.py b/homeassistant/components/plant.py index cdd3c8463aa..55793558fd9 100644 --- a/homeassistant/components/plant.py +++ b/homeassistant/components/plant.py @@ -56,6 +56,13 @@ CONF_SENSOR_CONDUCTIVITY = READING_CONDUCTIVITY CONF_SENSOR_TEMPERATURE = READING_TEMPERATURE CONF_SENSOR_BRIGHTNESS = READING_BRIGHTNESS +DEFAULT_MIN_BATTERY_LEVEL = 20 +DEFAULT_MIN_MOISTURE = 20 +DEFAULT_MAX_MOISTURE = 60 +DEFAULT_MIN_CONDUCTIVITY = 500 +DEFAULT_MAX_CONDUCTIVITY = 3000 +DEFAULT_CHECK_DAYS = 3 + SCHEMA_SENSORS = vol.Schema({ vol.Optional(CONF_SENSOR_BATTERY_LEVEL): cv.entity_id, vol.Optional(CONF_SENSOR_MOISTURE): cv.entity_id, @@ -66,16 +73,22 @@ SCHEMA_SENSORS = vol.Schema({ PLANT_SCHEMA = vol.Schema({ vol.Required(CONF_SENSORS): vol.Schema(SCHEMA_SENSORS), - vol.Optional(CONF_MIN_BATTERY_LEVEL): cv.positive_int, + vol.Optional(CONF_MIN_BATTERY_LEVEL, + default=DEFAULT_MIN_BATTERY_LEVEL): cv.positive_int, vol.Optional(CONF_MIN_TEMPERATURE): vol.Coerce(float), vol.Optional(CONF_MAX_TEMPERATURE): vol.Coerce(float), - vol.Optional(CONF_MIN_MOISTURE): cv.positive_int, - vol.Optional(CONF_MAX_MOISTURE): cv.positive_int, - vol.Optional(CONF_MIN_CONDUCTIVITY): cv.positive_int, - vol.Optional(CONF_MAX_CONDUCTIVITY): cv.positive_int, + vol.Optional(CONF_MIN_MOISTURE, + default=DEFAULT_MIN_MOISTURE): cv.positive_int, + vol.Optional(CONF_MAX_MOISTURE, + default=DEFAULT_MAX_MOISTURE): cv.positive_int, + vol.Optional(CONF_MIN_CONDUCTIVITY, + default=DEFAULT_MIN_CONDUCTIVITY): cv.positive_int, + vol.Optional(CONF_MAX_CONDUCTIVITY, + default=DEFAULT_MAX_CONDUCTIVITY): cv.positive_int, vol.Optional(CONF_MIN_BRIGHTNESS): cv.positive_int, vol.Optional(CONF_MAX_BRIGHTNESS): cv.positive_int, - vol.Optional(CONF_CHECK_DAYS): cv.positive_int, + vol.Optional(CONF_CHECK_DAYS, + default=DEFAULT_CHECK_DAYS): cv.positive_int, }) DOMAIN = 'plant' From e753ffca940574db3057373307c7ddee5d364afb Mon Sep 17 00:00:00 2001 From: Richard Mitchell Date: Thu, 10 Jan 2019 17:28:01 +0000 Subject: [PATCH 074/235] Correctly map Nest hvac_state to Home Assistant states. (#19895) --- homeassistant/components/sensor/nest.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/nest.py b/homeassistant/components/sensor/nest.py index 738bc53d880..5514203c6ea 100644 --- a/homeassistant/components/sensor/nest.py +++ b/homeassistant/components/sensor/nest.py @@ -6,11 +6,13 @@ https://home-assistant.io/components/sensor.nest/ """ import logging +from homeassistant.components.climate import ( + STATE_COOL, STATE_HEAT) from homeassistant.components.nest import ( DATA_NEST, DATA_NEST_CONFIG, CONF_SENSORS, NestSensorDevice) from homeassistant.const import ( TEMP_CELSIUS, TEMP_FAHRENHEIT, CONF_MONITORED_CONDITIONS, - DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_HUMIDITY) + DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_HUMIDITY, STATE_OFF) DEPENDENCIES = ['nest'] @@ -39,6 +41,10 @@ SENSOR_DEVICE_CLASSES = {'humidity': DEVICE_CLASS_HUMIDITY} VARIABLE_NAME_MAPPING = {'eta': 'eta_begin', 'operation_mode': 'mode'} +VALUE_MAPPING = { + 'hvac_state': { + 'heating': STATE_HEAT, 'cooling': STATE_COOL, 'off': STATE_OFF}} + SENSOR_TYPES_DEPRECATED = ['last_ip', 'local_ip', 'last_connection', @@ -142,6 +148,9 @@ class NestBasicSensor(NestSensorDevice): if self.variable in VARIABLE_NAME_MAPPING: self._state = getattr(self.device, VARIABLE_NAME_MAPPING[self.variable]) + elif self.variable in VALUE_MAPPING: + state = getattr(self.device, self.variable) + self._state = VALUE_MAPPING[self.variable].get(state, state) elif self.variable in PROTECT_SENSOR_TYPES \ and self.variable != 'color_status': # keep backward compatibility From d859c3fa86ebffdc28328c1bbee21abf8b3207b9 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Thu, 10 Jan 2019 14:43:24 -0500 Subject: [PATCH 075/235] Don't map LevelControl to light for single cluster devices. (#19929) --- homeassistant/components/zha/const.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/zha/const.py b/homeassistant/components/zha/const.py index 3e7f9f89f91..5b650c95cc4 100644 --- a/homeassistant/components/zha/const.py +++ b/homeassistant/components/zha/const.py @@ -129,7 +129,6 @@ def populate_data(): SINGLE_INPUT_CLUSTER_DEVICE_CLASS.update({ zcl.clusters.general.OnOff: 'switch', - zcl.clusters.general.LevelControl: 'light', zcl.clusters.measurement.RelativeHumidity: 'sensor', zcl.clusters.measurement.TemperatureMeasurement: 'sensor', zcl.clusters.measurement.PressureMeasurement: 'sensor', From 616f23ae1d9f6f9f6cadb1ecb037db20f5651532 Mon Sep 17 00:00:00 2001 From: "Clifford W. Hansen" Date: Thu, 10 Jan 2019 22:55:17 +0200 Subject: [PATCH 076/235] Add btle_name attribute to devices (#19915) * Update googlehome.py Added name from bluetooth device to attributes * Update homeassistant/components/device_tracker/googlehome.py Check if key exists before assigning name Co-Authored-By: cliffordwhansen --- homeassistant/components/device_tracker/googlehome.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/device_tracker/googlehome.py b/homeassistant/components/device_tracker/googlehome.py index dabb92a0751..daa36d1d2c7 100644 --- a/homeassistant/components/device_tracker/googlehome.py +++ b/homeassistant/components/device_tracker/googlehome.py @@ -89,5 +89,7 @@ class GoogleHomeDeviceScanner(DeviceScanner): devices[uuid]['btle_mac_address'] = device['mac_address'] devices[uuid]['ghname'] = ghname devices[uuid]['source_type'] = 'bluetooth' + if device['name']: + devices[uuid]['btle_name'] = device['name'] await self.scanner.clear_scan_result() self.last_results = devices From de76b59d0b139f42bb2a0aa424d1427e4e662d50 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Thu, 10 Jan 2019 22:50:39 +0100 Subject: [PATCH 077/235] Upgrade youtube_dl to 2019.01.10 --- 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 749935fda2b..940f2dd79ca 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.12.17'] +REQUIREMENTS = ['youtube_dl==2019.01.10'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index cadecc54eb4..2bd62a5ddfe 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1724,7 +1724,7 @@ yeelight==0.4.3 yeelightsunflower==0.0.10 # homeassistant.components.media_extractor -youtube_dl==2018.12.17 +youtube_dl==2019.01.10 # homeassistant.components.light.zengge zengge==0.2 From 6403a13ea36aa4d2c062ac0ff6ebbb9881067c05 Mon Sep 17 00:00:00 2001 From: Daniel Shokouhi Date: Thu, 10 Jan 2019 13:56:10 -0800 Subject: [PATCH 078/235] Fix botvac connected alert retrieval --- homeassistant/components/vacuum/neato.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/vacuum/neato.py b/homeassistant/components/vacuum/neato.py index 0c97f99df99..37d4783cdbd 100644 --- a/homeassistant/components/vacuum/neato.py +++ b/homeassistant/components/vacuum/neato.py @@ -82,7 +82,10 @@ class NeatoConnectedVacuum(StateVacuumDevice): self._available = False return _LOGGER.debug('self._state=%s', self._state) - robot_alert = ALERTS.get(self._state['alert']) + if 'alert' in self._state: + robot_alert = ALERTS.get(self._state['alert']) + else: + robot_alert = None if self._state['state'] == 1: if self._state['details']['isCharging']: self._clean_state = STATE_DOCKED From 6c2931508873f94862a7e1c45afde223b043ac1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ab=C3=ADlio=20Costa?= Date: Thu, 10 Jan 2019 23:52:21 +0000 Subject: [PATCH 079/235] Add Alexa's EndpointHealth reporting (#19784) * add Health reports * add health report for all devices * update tests * Update homeassistant/components/alexa/smart_home.py Co-Authored-By: abmantis * lint * add tests --- homeassistant/components/alexa/smart_home.py | 53 ++++++- tests/components/alexa/test_smart_home.py | 154 +++++++++++++++---- 2 files changed, 176 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/alexa/smart_home.py b/homeassistant/components/alexa/smart_home.py index 1558a1bf218..beb1a9c15ee 100644 --- a/homeassistant/components/alexa/smart_home.py +++ b/homeassistant/components/alexa/smart_home.py @@ -27,8 +27,9 @@ from homeassistant.const import ( CONF_NAME, SERVICE_LOCK, SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_PLAY, SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_MEDIA_STOP, SERVICE_SET_COVER_POSITION, SERVICE_TURN_OFF, SERVICE_TURN_ON, - SERVICE_UNLOCK, SERVICE_VOLUME_SET, STATE_LOCKED, STATE_ON, STATE_UNLOCKED, - TEMP_CELSIUS, TEMP_FAHRENHEIT, MATCH_ALL) + SERVICE_UNLOCK, SERVICE_VOLUME_SET, STATE_LOCKED, STATE_ON, + STATE_UNAVAILABLE, STATE_UNLOCKED, TEMP_CELSIUS, TEMP_FAHRENHEIT, + MATCH_ALL) import homeassistant.core as ha import homeassistant.util.color as color_util from homeassistant.util.decorator import Registry @@ -393,6 +394,37 @@ class _AlexaInterface: } +class _AlexaEndpointHealth(_AlexaInterface): + """Implements Alexa.EndpointHealth. + + https://developer.amazon.com/docs/smarthome/state-reporting-for-a-smart-home-skill.html#report-state-when-alexa-requests-it + """ + + def __init__(self, hass, entity): + super().__init__(entity) + self.hass = hass + + def name(self): + return 'Alexa.EndpointHealth' + + def properties_supported(self): + return [{'name': 'connectivity'}] + + def properties_proactively_reported(self): + return False + + def properties_retrievable(self): + return True + + def get_property(self, name): + if name != 'connectivity': + raise _UnsupportedProperty(name) + + if self.entity.state == STATE_UNAVAILABLE: + return {'value': 'UNREACHABLE'} + return {'value': 'OK'} + + class _AlexaPowerController(_AlexaInterface): """Implements Alexa.PowerController. @@ -769,7 +801,8 @@ class _GenericCapabilities(_AlexaEntity): return [_DisplayCategory.OTHER] def interfaces(self): - return [_AlexaPowerController(self.entity)] + return [_AlexaPowerController(self.entity), + _AlexaEndpointHealth(self.hass, self.entity)] @ENTITY_ADAPTERS.register(switch.DOMAIN) @@ -778,7 +811,8 @@ class _SwitchCapabilities(_AlexaEntity): return [_DisplayCategory.SWITCH] def interfaces(self): - return [_AlexaPowerController(self.entity)] + return [_AlexaPowerController(self.entity), + _AlexaEndpointHealth(self.hass, self.entity)] @ENTITY_ADAPTERS.register(climate.DOMAIN) @@ -792,6 +826,7 @@ class _ClimateCapabilities(_AlexaEntity): yield _AlexaPowerController(self.entity) yield _AlexaThermostatController(self.hass, self.entity) yield _AlexaTemperatureSensor(self.hass, self.entity) + yield _AlexaEndpointHealth(self.hass, self.entity) @ENTITY_ADAPTERS.register(cover.DOMAIN) @@ -804,6 +839,7 @@ class _CoverCapabilities(_AlexaEntity): supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) if supported & cover.SUPPORT_SET_POSITION: yield _AlexaPercentageController(self.entity) + yield _AlexaEndpointHealth(self.hass, self.entity) @ENTITY_ADAPTERS.register(light.DOMAIN) @@ -821,6 +857,7 @@ class _LightCapabilities(_AlexaEntity): yield _AlexaColorController(self.entity) if supported & light.SUPPORT_COLOR_TEMP: yield _AlexaColorTemperatureController(self.entity) + yield _AlexaEndpointHealth(self.hass, self.entity) @ENTITY_ADAPTERS.register(fan.DOMAIN) @@ -833,6 +870,7 @@ class _FanCapabilities(_AlexaEntity): supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) if supported & fan.SUPPORT_SET_SPEED: yield _AlexaPercentageController(self.entity) + yield _AlexaEndpointHealth(self.hass, self.entity) @ENTITY_ADAPTERS.register(lock.DOMAIN) @@ -841,7 +879,8 @@ class _LockCapabilities(_AlexaEntity): return [_DisplayCategory.SMARTLOCK] def interfaces(self): - return [_AlexaLockController(self.entity)] + return [_AlexaLockController(self.entity), + _AlexaEndpointHealth(self.hass, self.entity)] @ENTITY_ADAPTERS.register(media_player.DOMAIN) @@ -851,6 +890,7 @@ class _MediaPlayerCapabilities(_AlexaEntity): def interfaces(self): yield _AlexaPowerController(self.entity) + yield _AlexaEndpointHealth(self.hass, self.entity) supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) if supported & media_player.SUPPORT_VOLUME_SET: @@ -913,6 +953,7 @@ class _SensorCapabilities(_AlexaEntity): TEMP_CELSIUS, ): yield _AlexaTemperatureSensor(self.hass, self.entity) + yield _AlexaEndpointHealth(self.hass, self.entity) @ENTITY_ADAPTERS.register(binary_sensor.DOMAIN) @@ -934,6 +975,8 @@ class _BinarySensorCapabilities(_AlexaEntity): elif sensor_type is self.TYPE_MOTION: yield _AlexaMotionSensor(self.hass, self.entity) + yield _AlexaEndpointHealth(self.hass, self.entity) + def get_type(self): """Return the type of binary sensor.""" attrs = self.entity.attributes diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index 93551076461..536112e9b0b 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -153,6 +153,15 @@ async def discovery_test(device, hass, expected_endpoints=1): return None +def get_capability(capabilities, capability_name): + """Search a set of capabilities for a specific one.""" + for capability in capabilities: + if capability['interface'] == capability_name: + return capability + + return None + + def assert_endpoint_capabilities(endpoint, *interfaces): """Assert the endpoint supports the given interfaces. @@ -176,7 +185,11 @@ async def test_switch(hass, events): assert appliance['endpointId'] == 'switch#test' assert appliance['displayCategories'][0] == "SWITCH" assert appliance['friendlyName'] == "Test switch" - assert_endpoint_capabilities(appliance, 'Alexa.PowerController') + assert_endpoint_capabilities( + appliance, + 'Alexa.PowerController', + 'Alexa.EndpointHealth', + ) await assert_power_controller_works( 'switch#test', @@ -196,7 +209,11 @@ async def test_light(hass): assert appliance['endpointId'] == 'light#test_1' assert appliance['displayCategories'][0] == "LIGHT" assert appliance['friendlyName'] == "Test light 1" - assert_endpoint_capabilities(appliance, 'Alexa.PowerController') + assert_endpoint_capabilities( + appliance, + 'Alexa.PowerController', + 'Alexa.EndpointHealth', + ) await assert_power_controller_works( 'light#test_1', @@ -222,6 +239,7 @@ async def test_dimmable_light(hass): appliance, 'Alexa.BrightnessController', 'Alexa.PowerController', + 'Alexa.EndpointHealth', ) properties = await reported_properties(hass, 'light#test_2') @@ -260,6 +278,7 @@ async def test_color_light(hass): 'Alexa.PowerController', 'Alexa.ColorController', 'Alexa.ColorTemperatureController', + 'Alexa.EndpointHealth', ) # IncreaseColorTemperature and DecreaseColorTemperature have their own @@ -277,7 +296,8 @@ async def test_script(hass): (capability,) = assert_endpoint_capabilities( appliance, - 'Alexa.SceneController') + 'Alexa.SceneController', + ) assert not capability['supportsDeactivation'] await assert_scene_controller_works( @@ -299,7 +319,8 @@ async def test_cancelable_script(hass): assert appliance['endpointId'] == 'script#test_2' (capability,) = assert_endpoint_capabilities( appliance, - 'Alexa.SceneController') + 'Alexa.SceneController', + ) assert capability['supportsDeactivation'] await assert_scene_controller_works( @@ -321,7 +342,11 @@ async def test_input_boolean(hass): assert appliance['endpointId'] == 'input_boolean#test' assert appliance['displayCategories'][0] == "OTHER" assert appliance['friendlyName'] == "Test input boolean" - assert_endpoint_capabilities(appliance, 'Alexa.PowerController') + assert_endpoint_capabilities( + appliance, + 'Alexa.PowerController', + 'Alexa.EndpointHealth', + ) await assert_power_controller_works( 'input_boolean#test', @@ -341,7 +366,8 @@ async def test_scene(hass): (capability,) = assert_endpoint_capabilities( appliance, - 'Alexa.SceneController') + 'Alexa.SceneController' + ) assert not capability['supportsDeactivation'] await assert_scene_controller_works( @@ -359,7 +385,11 @@ async def test_fan(hass): assert appliance['endpointId'] == 'fan#test_1' assert appliance['displayCategories'][0] == "OTHER" assert appliance['friendlyName'] == "Test fan 1" - assert_endpoint_capabilities(appliance, 'Alexa.PowerController') + assert_endpoint_capabilities( + appliance, + 'Alexa.PowerController', + 'Alexa.EndpointHealth', + ) async def test_variable_fan(hass): @@ -386,6 +416,7 @@ async def test_variable_fan(hass): appliance, 'Alexa.PercentageController', 'Alexa.PowerController', + 'Alexa.EndpointHealth', ) call, _ = await assert_request_calls_service( @@ -412,7 +443,11 @@ async def test_lock(hass): assert appliance['endpointId'] == 'lock#test' assert appliance['displayCategories'][0] == "SMARTLOCK" assert appliance['friendlyName'] == "Test lock" - assert_endpoint_capabilities(appliance, 'Alexa.LockController') + assert_endpoint_capabilities( + appliance, + 'Alexa.LockController', + 'Alexa.EndpointHealth', + ) _, msg = await assert_request_calls_service( 'Alexa.LockController', 'Lock', 'lock#test', @@ -449,6 +484,7 @@ async def test_media_player(hass): 'Alexa.Speaker', 'Alexa.StepSpeaker', 'Alexa.PlaybackController', + 'Alexa.EndpointHealth', ) await assert_power_controller_works( @@ -546,7 +582,11 @@ async def test_alert(hass): assert appliance['endpointId'] == 'alert#test' assert appliance['displayCategories'][0] == "OTHER" assert appliance['friendlyName'] == "Test alert" - assert_endpoint_capabilities(appliance, 'Alexa.PowerController') + assert_endpoint_capabilities( + appliance, + 'Alexa.PowerController', + 'Alexa.EndpointHealth', + ) await assert_power_controller_works( 'alert#test', @@ -563,7 +603,11 @@ async def test_automation(hass): assert appliance['endpointId'] == 'automation#test' assert appliance['displayCategories'][0] == "OTHER" assert appliance['friendlyName'] == "Test automation" - assert_endpoint_capabilities(appliance, 'Alexa.PowerController') + assert_endpoint_capabilities( + appliance, + 'Alexa.PowerController', + 'Alexa.EndpointHealth', + ) await assert_power_controller_works( 'automation#test', @@ -580,7 +624,11 @@ async def test_group(hass): assert appliance['endpointId'] == 'group#test' assert appliance['displayCategories'][0] == "OTHER" assert appliance['friendlyName'] == "Test group" - assert_endpoint_capabilities(appliance, 'Alexa.PowerController') + assert_endpoint_capabilities( + appliance, + 'Alexa.PowerController', + 'Alexa.EndpointHealth', + ) await assert_power_controller_works( 'group#test', @@ -609,6 +657,7 @@ async def test_cover(hass): appliance, 'Alexa.PercentageController', 'Alexa.PowerController', + 'Alexa.EndpointHealth', ) await assert_power_controller_works( @@ -675,11 +724,16 @@ async def test_temp_sensor(hass): assert appliance['displayCategories'][0] == 'TEMPERATURE_SENSOR' assert appliance['friendlyName'] == 'Test Temp Sensor' - (capability,) = assert_endpoint_capabilities( + capabilities = assert_endpoint_capabilities( appliance, - 'Alexa.TemperatureSensor') - assert capability['interface'] == 'Alexa.TemperatureSensor' - properties = capability['properties'] + 'Alexa.TemperatureSensor', + 'Alexa.EndpointHealth', + ) + + temp_sensor_capability = get_capability(capabilities, + 'Alexa.TemperatureSensor') + assert temp_sensor_capability is not None + properties = temp_sensor_capability['properties'] assert properties['retrievable'] is True assert {'name': 'temperature'} in properties['supported'] @@ -704,11 +758,16 @@ async def test_contact_sensor(hass): assert appliance['displayCategories'][0] == 'CONTACT_SENSOR' assert appliance['friendlyName'] == 'Test Contact Sensor' - (capability,) = assert_endpoint_capabilities( + capabilities = assert_endpoint_capabilities( appliance, - 'Alexa.ContactSensor') - assert capability['interface'] == 'Alexa.ContactSensor' - properties = capability['properties'] + 'Alexa.ContactSensor', + 'Alexa.EndpointHealth', + ) + + contact_sensor_capability = get_capability(capabilities, + 'Alexa.ContactSensor') + assert contact_sensor_capability is not None + properties = contact_sensor_capability['properties'] assert properties['retrievable'] is True assert {'name': 'detectionState'} in properties['supported'] @@ -717,6 +776,9 @@ async def test_contact_sensor(hass): properties.assert_equal('Alexa.ContactSensor', 'detectionState', 'DETECTED') + properties.assert_equal('Alexa.EndpointHealth', 'connectivity', + {'value': 'OK'}) + async def test_motion_sensor(hass): """Test motion sensor discovery.""" @@ -734,11 +796,16 @@ async def test_motion_sensor(hass): assert appliance['displayCategories'][0] == 'MOTION_SENSOR' assert appliance['friendlyName'] == 'Test Motion Sensor' - (capability,) = assert_endpoint_capabilities( + capabilities = assert_endpoint_capabilities( appliance, - 'Alexa.MotionSensor') - assert capability['interface'] == 'Alexa.MotionSensor' - properties = capability['properties'] + 'Alexa.MotionSensor', + 'Alexa.EndpointHealth', + ) + + motion_sensor_capability = get_capability(capabilities, + 'Alexa.MotionSensor') + assert motion_sensor_capability is not None + properties = motion_sensor_capability['properties'] assert properties['retrievable'] is True assert {'name': 'detectionState'} in properties['supported'] @@ -787,6 +854,7 @@ async def test_thermostat(hass): appliance, 'Alexa.ThermostatController', 'Alexa.TemperatureSensor', + 'Alexa.EndpointHealth', ) properties = await reported_properties( @@ -1486,9 +1554,11 @@ async def test_entity_config(hass): assert appliance['displayCategories'][0] == "SWITCH" assert appliance['friendlyName'] == "Config name" assert appliance['description'] == "Config description" - assert len(appliance['capabilities']) == 1 - assert appliance['capabilities'][-1]['interface'] == \ - 'Alexa.PowerController' + assert_endpoint_capabilities( + appliance, + 'Alexa.PowerController', + 'Alexa.EndpointHealth', + ) async def test_unsupported_domain(hass): @@ -1651,6 +1721,38 @@ async def test_disabled(hass): assert msg['payload']['type'] == 'BRIDGE_UNREACHABLE' +async def test_endpoint_good_health(hass): + """Test endpoint health reporting.""" + device = ( + 'binary_sensor.test_contact', + 'on', + { + 'friendly_name': "Test Contact Sensor", + 'device_class': 'door', + } + ) + await discovery_test(device, hass) + properties = await reported_properties(hass, 'binary_sensor#test_contact') + properties.assert_equal('Alexa.EndpointHealth', 'connectivity', + {'value': 'OK'}) + + +async def test_endpoint_bad_health(hass): + """Test endpoint health reporting.""" + device = ( + 'binary_sensor.test_contact', + 'unavailable', + { + 'friendly_name': "Test Contact Sensor", + 'device_class': 'door', + } + ) + await discovery_test(device, hass) + properties = await reported_properties(hass, 'binary_sensor#test_contact') + properties.assert_equal('Alexa.EndpointHealth', 'connectivity', + {'value': 'UNREACHABLE'}) + + async def test_report_state(hass, aioclient_mock): """Test proactive state reports.""" aioclient_mock.post(TEST_URL, json={'data': 'is irrelevant'}) From 4d52adb008371675333a156baff362c4d35a0da7 Mon Sep 17 00:00:00 2001 From: so3n Date: Fri, 11 Jan 2019 10:53:12 +1100 Subject: [PATCH 080/235] Remove Discovery dependency from konnected.py (#19910) --- homeassistant/components/konnected.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/konnected.py b/homeassistant/components/konnected.py index a6fe4be65fa..0cba8552346 100644 --- a/homeassistant/components/konnected.py +++ b/homeassistant/components/konnected.py @@ -96,7 +96,7 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) -DEPENDENCIES = ['http', 'discovery'] +DEPENDENCIES = ['http'] ENDPOINT_ROOT = '/api/konnected' UPDATE_ENDPOINT = (ENDPOINT_ROOT + r'/device/{device_id:[a-zA-Z0-9]+}') From 4d187e08d460fe7077dab8e226fe940ff253f6db Mon Sep 17 00:00:00 2001 From: arigilder <43716164+arigilder@users.noreply.github.com> Date: Thu, 10 Jan 2019 19:27:34 -0500 Subject: [PATCH 081/235] Add sensors to jewish_calendar for upcoming Shabbat times (#19278) * Initial pass of cleanup for shabbat_times * Switch to async defs * First pass of unit tests + fixture data * Completion of first round of unit tests, 100% passing * Unit tests for state restoring * Style fixes * More style fixes * Lint fix * Add upcoming candelighting and havdalah sensors * Add unit tests, remove havdalah offset * More unit tests + small bugfix for weekly_portion * Add issur melacha sensor * Remove old shabbat_times work-in-progress files * Bump required version of hdate * Add havdalah offset config parameter * Bump hdate version required * Pin hdate requirement * Lint fixes * Changes based on review + API changes for hdate 0.8.7 * Add three-day holiday unit tests * Remove debugging line * Add missing docstring * Fix doc lint comment --- .../components/sensor/jewish_calendar.py | 63 +++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../components/sensor/test_jewish_calendar.py | 332 +++++++++++++++++- 4 files changed, 382 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/sensor/jewish_calendar.py b/homeassistant/components/sensor/jewish_calendar.py index 8058a411266..cc226337f02 100644 --- a/homeassistant/components/sensor/jewish_calendar.py +++ b/homeassistant/components/sensor/jewish_calendar.py @@ -17,7 +17,7 @@ from homeassistant.helpers.entity import Entity from homeassistant.helpers.sun import get_astral_event_date import homeassistant.util.dt as dt_util -REQUIREMENTS = ['hdate==0.7.5'] +REQUIREMENTS = ['hdate==0.8.7'] _LOGGER = logging.getLogger(__name__) @@ -31,11 +31,24 @@ SENSOR_TYPES = { 'mga_end_shma': ['Latest time for Shm"a MG"A', 'mdi:calendar-clock'], 'plag_mincha': ['Plag Hamincha', 'mdi:weather-sunset-down'], 'first_stars': ['T\'set Hakochavim', 'mdi:weather-night'], + 'upcoming_shabbat_candle_lighting': ['Upcoming Shabbat Candle Lighting', + 'mdi:candle'], + 'upcoming_shabbat_havdalah': ['Upcoming Shabbat Havdalah', + 'mdi:weather-night'], + 'upcoming_candle_lighting': ['Upcoming Candle Lighting', 'mdi:candle'], + 'upcoming_havdalah': ['Upcoming Havdalah', 'mdi:weather-night'], + 'issur_melacha_in_effect': ['Issur Melacha in Effect', + 'mdi:power-plug-off'], + 'omer_count': ['Day of the Omer', 'mdi:counter'], } CONF_DIASPORA = 'diaspora' CONF_LANGUAGE = 'language' CONF_SENSORS = 'sensors' +CONF_CANDLE_LIGHT_MINUTES = 'candle_lighting_minutes_before_sunset' +CONF_HAVDALAH_OFFSET_MINUTES = 'havdalah_minutes_after_sunset' + +CANDLE_LIGHT_DEFAULT = 18 DEFAULT_NAME = 'Jewish Calendar' @@ -46,6 +59,9 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_LONGITUDE): cv.longitude, vol.Optional(CONF_LANGUAGE, default='english'): vol.In(['hebrew', 'english']), + vol.Optional(CONF_CANDLE_LIGHT_MINUTES, default=CANDLE_LIGHT_DEFAULT): int, + # Default of 0 means use 8.5 degrees / 'three_stars' time. + vol.Optional(CONF_HAVDALAH_OFFSET_MINUTES, default=0): int, vol.Optional(CONF_SENSORS, default=['date']): vol.All(cv.ensure_list, vol.Length(min=1), [vol.In(SENSOR_TYPES)]), }) @@ -59,6 +75,8 @@ async def async_setup_platform( latitude = config.get(CONF_LATITUDE, hass.config.latitude) longitude = config.get(CONF_LONGITUDE, hass.config.longitude) diaspora = config.get(CONF_DIASPORA) + candle_lighting_offset = config.get(CONF_CANDLE_LIGHT_MINUTES) + havdalah_offset = config.get(CONF_HAVDALAH_OFFSET_MINUTES) if None in (latitude, longitude): _LOGGER.error("Latitude or longitude not set in Home Assistant config") @@ -68,7 +86,8 @@ async def async_setup_platform( for sensor_type in config[CONF_SENSORS]: dev.append(JewishCalSensor( name, language, sensor_type, latitude, longitude, - hass.config.time_zone, diaspora)) + hass.config.time_zone, diaspora, candle_lighting_offset, + havdalah_offset)) async_add_entities(dev, True) @@ -77,7 +96,8 @@ class JewishCalSensor(Entity): def __init__( self, name, language, sensor_type, latitude, longitude, timezone, - diaspora): + diaspora, candle_lighting_offset=CANDLE_LIGHT_DEFAULT, + havdalah_offset=0): """Initialize the Jewish calendar sensor.""" self.client_name = name self._name = SENSOR_TYPES[sensor_type][0] @@ -88,6 +108,8 @@ class JewishCalSensor(Entity): self.longitude = longitude self.timezone = timezone self.diaspora = diaspora + self.candle_lighting_offset = candle_lighting_offset + self.havdalah_offset = havdalah_offset _LOGGER.debug("Sensor %s initialized", self.type) @property @@ -124,18 +146,45 @@ class JewishCalSensor(Entity): date = hdate.HDate( today, diaspora=self.diaspora, hebrew=self._hebrew) + location = hdate.Location(latitude=self.latitude, + longitude=self.longitude, + timezone=self.timezone, + diaspora=self.diaspora) + + def make_zmanim(date): + """Create a Zmanim object.""" + return hdate.Zmanim( + date=date, location=location, + candle_lighting_offset=self.candle_lighting_offset, + havdalah_offset=self.havdalah_offset, hebrew=self._hebrew) + if self.type == 'date': self._state = date.hebrew_date elif self.type == 'weekly_portion': - self._state = date.parasha + # Compute the weekly portion based on the upcoming shabbat. + self._state = date.upcoming_shabbat.parasha elif self.type == 'holiday_name': self._state = date.holiday_description elif self.type == 'holyness': self._state = date.holiday_type + elif self.type == 'upcoming_shabbat_candle_lighting': + times = make_zmanim(date.upcoming_shabbat.previous_day.gdate) + self._state = times.candle_lighting + elif self.type == 'upcoming_candle_lighting': + times = make_zmanim(date.upcoming_shabbat_or_yom_tov.first_day + .previous_day.gdate) + self._state = times.candle_lighting + elif self.type == 'upcoming_shabbat_havdalah': + times = make_zmanim(date.upcoming_shabbat.gdate) + self._state = times.havdalah + elif self.type == 'upcoming_havdalah': + times = make_zmanim(date.upcoming_shabbat_or_yom_tov + .last_day.gdate) + self._state = times.havdalah + elif self.type == 'issur_melacha_in_effect': + self._state = make_zmanim(now).issur_melacha_in_effect else: - times = hdate.Zmanim( - date=today, latitude=self.latitude, longitude=self.longitude, - timezone=self.timezone, hebrew=self._hebrew).zmanim + times = make_zmanim(today).zmanim self._state = times[self.type].time() _LOGGER.debug("New value: %s", self._state) diff --git a/requirements_all.txt b/requirements_all.txt index 2bd62a5ddfe..45884ccf030 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -487,7 +487,7 @@ hangups==0.4.6 hbmqtt==0.9.4 # homeassistant.components.sensor.jewish_calendar -hdate==0.7.5 +hdate==0.8.7 # homeassistant.components.climate.heatmiser heatmiserV3==0.9.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cb533e35227..7886047f4b2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -98,7 +98,7 @@ hangups==0.4.6 hbmqtt==0.9.4 # homeassistant.components.sensor.jewish_calendar -hdate==0.7.5 +hdate==0.8.7 # homeassistant.components.binary_sensor.workday holidays==0.9.9 diff --git a/tests/components/sensor/test_jewish_calendar.py b/tests/components/sensor/test_jewish_calendar.py index 320bc903661..639364164e0 100644 --- a/tests/components/sensor/test_jewish_calendar.py +++ b/tests/components/sensor/test_jewish_calendar.py @@ -1,4 +1,5 @@ """The tests for the Jewish calendar sensor platform.""" +from collections import namedtuple from datetime import time from datetime import datetime as dt from unittest.mock import patch @@ -8,13 +9,34 @@ import pytest from homeassistant.util.async_ import run_coroutine_threadsafe from homeassistant.util.dt import get_time_zone, set_default_time_zone from homeassistant.setup import setup_component -from homeassistant.components.sensor.jewish_calendar import JewishCalSensor +from homeassistant.components.sensor.jewish_calendar import ( + JewishCalSensor, CANDLE_LIGHT_DEFAULT) from tests.common import get_test_home_assistant +_LatLng = namedtuple('_LatLng', ['lat', 'lng']) + +NYC_LATLNG = _LatLng(40.7128, -74.0060) +JERUSALEM_LATLNG = _LatLng(31.778, 35.235) + + +def make_nyc_test_params(dtime, results, havdalah_offset=0): + """Make test params for NYC.""" + return (dtime, CANDLE_LIGHT_DEFAULT, havdalah_offset, True, + 'America/New_York', NYC_LATLNG.lat, NYC_LATLNG.lng, results) + + +def make_jerusalem_test_params(dtime, results, havdalah_offset=0): + """Make test params for Jerusalem.""" + return (dtime, CANDLE_LIGHT_DEFAULT, havdalah_offset, False, + 'Asia/Jerusalem', JERUSALEM_LATLNG.lat, JERUSALEM_LATLNG.lng, + results) + + class TestJewishCalenderSensor(): """Test the Jewish Calendar sensor.""" + # pylint: disable=attribute-defined-outside-init def setup_method(self, method): """Set up things to run when tests begin.""" self.hass = get_test_home_assistant() @@ -99,21 +121,315 @@ class TestJewishCalenderSensor(): "date_after_sunset" ] - @pytest.mark.parametrize(["time", "tzname", "latitude", "longitude", + @pytest.mark.parametrize(["cur_time", "tzname", "latitude", "longitude", "language", "sensor", "diaspora", "result"], test_params, ids=test_ids) - def test_jewish_calendar_sensor(self, time, tzname, latitude, longitude, - language, sensor, diaspora, result): + def test_jewish_calendar_sensor(self, cur_time, tzname, latitude, + longitude, language, sensor, diaspora, + result): """Test Jewish calendar sensor output.""" - tz = get_time_zone(tzname) - set_default_time_zone(tz) - test_time = tz.localize(time) + time_zone = get_time_zone(tzname) + set_default_time_zone(time_zone) + test_time = time_zone.localize(cur_time) self.hass.config.latitude = latitude self.hass.config.longitude = longitude sensor = JewishCalSensor( name='test', language=language, sensor_type=sensor, latitude=latitude, longitude=longitude, - timezone=tz, diaspora=diaspora) + timezone=time_zone, diaspora=diaspora) + sensor.hass = self.hass + with patch('homeassistant.util.dt.now', return_value=test_time): + run_coroutine_threadsafe( + sensor.async_update(), + self.hass.loop).result() + assert sensor.state == result + + shabbat_params = [ + make_nyc_test_params( + dt(2018, 9, 1, 16, 0), + {'upcoming_shabbat_candle_lighting': dt(2018, 8, 31, 19, 15), + 'upcoming_shabbat_havdalah': dt(2018, 9, 1, 20, 14), + 'weekly_portion': 'Ki Tavo', + 'hebrew_weekly_portion': 'כי תבוא'}), + make_nyc_test_params( + dt(2018, 9, 1, 16, 0), + {'upcoming_shabbat_candle_lighting': dt(2018, 8, 31, 19, 15), + 'upcoming_shabbat_havdalah': dt(2018, 9, 1, 20, 22), + 'weekly_portion': 'Ki Tavo', + 'hebrew_weekly_portion': 'כי תבוא'}, + havdalah_offset=50), + make_nyc_test_params( + dt(2018, 9, 1, 20, 21), + {'upcoming_shabbat_candle_lighting': dt(2018, 9, 7, 19, 4), + 'upcoming_shabbat_havdalah': dt(2018, 9, 8, 20, 2), + 'weekly_portion': 'Nitzavim', + 'hebrew_weekly_portion': 'נצבים'}), + make_nyc_test_params( + dt(2018, 9, 7, 13, 1), + {'upcoming_shabbat_candle_lighting': dt(2018, 9, 7, 19, 4), + 'upcoming_shabbat_havdalah': dt(2018, 9, 8, 20, 2), + 'weekly_portion': 'Nitzavim', + 'hebrew_weekly_portion': 'נצבים'}), + make_nyc_test_params( + dt(2018, 9, 8, 21, 25), + {'upcoming_candle_lighting': dt(2018, 9, 9, 19, 1), + 'upcoming_havdalah': dt(2018, 9, 11, 19, 57), + 'upcoming_shabbat_candle_lighting': dt(2018, 9, 14, 18, 52), + 'upcoming_shabbat_havdalah': dt(2018, 9, 15, 19, 50), + 'weekly_portion': 'Vayeilech', + 'hebrew_weekly_portion': 'וילך', + 'holiday_name': 'Erev Rosh Hashana', + 'hebrew_holiday_name': 'ערב ראש השנה'}), + make_nyc_test_params( + dt(2018, 9, 9, 21, 25), + {'upcoming_candle_lighting': dt(2018, 9, 9, 19, 1), + 'upcoming_havdalah': dt(2018, 9, 11, 19, 57), + 'upcoming_shabbat_candle_lighting': dt(2018, 9, 14, 18, 52), + 'upcoming_shabbat_havdalah': dt(2018, 9, 15, 19, 50), + 'weekly_portion': 'Vayeilech', + 'hebrew_weekly_portion': 'וילך', + 'holiday_name': 'Rosh Hashana I', + 'hebrew_holiday_name': "א' ראש השנה"}), + make_nyc_test_params( + dt(2018, 9, 10, 21, 25), + {'upcoming_candle_lighting': dt(2018, 9, 9, 19, 1), + 'upcoming_havdalah': dt(2018, 9, 11, 19, 57), + 'upcoming_shabbat_candle_lighting': dt(2018, 9, 14, 18, 52), + 'upcoming_shabbat_havdalah': dt(2018, 9, 15, 19, 50), + 'weekly_portion': 'Vayeilech', + 'hebrew_weekly_portion': 'וילך', + 'holiday_name': 'Rosh Hashana II', + 'hebrew_holiday_name': "ב' ראש השנה"}), + make_nyc_test_params( + dt(2018, 9, 28, 21, 25), + {'upcoming_shabbat_candle_lighting': dt(2018, 9, 28, 18, 28), + 'upcoming_shabbat_havdalah': dt(2018, 9, 29, 19, 25), + 'weekly_portion': 'none', + 'hebrew_weekly_portion': 'none'}), + make_nyc_test_params( + dt(2018, 9, 29, 21, 25), + {'upcoming_candle_lighting': dt(2018, 9, 30, 18, 25), + 'upcoming_havdalah': dt(2018, 10, 2, 19, 20), + 'upcoming_shabbat_candle_lighting': dt(2018, 10, 5, 18, 17), + 'upcoming_shabbat_havdalah': dt(2018, 10, 6, 19, 13), + 'weekly_portion': 'Bereshit', + 'hebrew_weekly_portion': 'בראשית', + 'holiday_name': 'Hoshana Raba', + 'hebrew_holiday_name': 'הושענא רבה'}), + make_nyc_test_params( + dt(2018, 9, 30, 21, 25), + {'upcoming_candle_lighting': dt(2018, 9, 30, 18, 25), + 'upcoming_havdalah': dt(2018, 10, 2, 19, 20), + 'upcoming_shabbat_candle_lighting': dt(2018, 10, 5, 18, 17), + 'upcoming_shabbat_havdalah': dt(2018, 10, 6, 19, 13), + 'weekly_portion': 'Bereshit', + 'hebrew_weekly_portion': 'בראשית', + 'holiday_name': 'Shmini Atzeret', + 'hebrew_holiday_name': 'שמיני עצרת'}), + make_nyc_test_params( + dt(2018, 10, 1, 21, 25), + {'upcoming_candle_lighting': dt(2018, 9, 30, 18, 25), + 'upcoming_havdalah': dt(2018, 10, 2, 19, 20), + 'upcoming_shabbat_candle_lighting': dt(2018, 10, 5, 18, 17), + 'upcoming_shabbat_havdalah': dt(2018, 10, 6, 19, 13), + 'weekly_portion': 'Bereshit', + 'hebrew_weekly_portion': 'בראשית', + 'holiday_name': 'Simchat Torah', + 'hebrew_holiday_name': 'שמחת תורה'}), + make_jerusalem_test_params( + dt(2018, 9, 29, 21, 25), + {'upcoming_candle_lighting': dt(2018, 9, 30, 18, 10), + 'upcoming_havdalah': dt(2018, 10, 1, 19, 2), + 'upcoming_shabbat_candle_lighting': dt(2018, 10, 5, 18, 3), + 'upcoming_shabbat_havdalah': dt(2018, 10, 6, 18, 56), + 'weekly_portion': 'Bereshit', + 'hebrew_weekly_portion': 'בראשית', + 'holiday_name': 'Hoshana Raba', + 'hebrew_holiday_name': 'הושענא רבה'}), + make_jerusalem_test_params( + dt(2018, 9, 30, 21, 25), + {'upcoming_candle_lighting': dt(2018, 9, 30, 18, 10), + 'upcoming_havdalah': dt(2018, 10, 1, 19, 2), + 'upcoming_shabbat_candle_lighting': dt(2018, 10, 5, 18, 3), + 'upcoming_shabbat_havdalah': dt(2018, 10, 6, 18, 56), + 'weekly_portion': 'Bereshit', + 'hebrew_weekly_portion': 'בראשית', + 'holiday_name': 'Shmini Atzeret', + 'hebrew_holiday_name': 'שמיני עצרת'}), + make_jerusalem_test_params( + dt(2018, 10, 1, 21, 25), + {'upcoming_shabbat_candle_lighting': dt(2018, 10, 5, 18, 3), + 'upcoming_shabbat_havdalah': dt(2018, 10, 6, 18, 56), + 'weekly_portion': 'Bereshit', + 'hebrew_weekly_portion': 'בראשית'}), + make_nyc_test_params( + dt(2016, 6, 11, 8, 25), + {'upcoming_candle_lighting': dt(2016, 6, 10, 20, 7), + 'upcoming_havdalah': dt(2016, 6, 13, 21, 17), + 'upcoming_shabbat_candle_lighting': dt(2016, 6, 10, 20, 7), + 'upcoming_shabbat_havdalah': None, + 'weekly_portion': 'Bamidbar', + 'hebrew_weekly_portion': 'במדבר', + 'holiday_name': 'Erev Shavuot', + 'hebrew_holiday_name': 'ערב שבועות'}), + make_nyc_test_params( + dt(2016, 6, 12, 8, 25), + {'upcoming_candle_lighting': dt(2016, 6, 10, 20, 7), + 'upcoming_havdalah': dt(2016, 6, 13, 21, 17), + 'upcoming_shabbat_candle_lighting': dt(2016, 6, 17, 20, 10), + 'upcoming_shabbat_havdalah': dt(2016, 6, 18, 21, 19), + 'weekly_portion': 'Nasso', + 'hebrew_weekly_portion': 'נשא', + 'holiday_name': 'Shavuot', + 'hebrew_holiday_name': 'שבועות'}), + make_jerusalem_test_params( + dt(2017, 9, 21, 8, 25), + {'upcoming_candle_lighting': dt(2017, 9, 20, 18, 23), + 'upcoming_havdalah': dt(2017, 9, 23, 19, 13), + 'upcoming_shabbat_candle_lighting': dt(2017, 9, 22, 19, 14), + 'upcoming_shabbat_havdalah': dt(2017, 9, 23, 19, 13), + 'weekly_portion': "Ha'Azinu", + 'hebrew_weekly_portion': 'האזינו', + 'holiday_name': 'Rosh Hashana I', + 'hebrew_holiday_name': "א' ראש השנה"}), + make_jerusalem_test_params( + dt(2017, 9, 22, 8, 25), + {'upcoming_candle_lighting': dt(2017, 9, 20, 18, 23), + 'upcoming_havdalah': dt(2017, 9, 23, 19, 13), + 'upcoming_shabbat_candle_lighting': dt(2017, 9, 22, 19, 14), + 'upcoming_shabbat_havdalah': dt(2017, 9, 23, 19, 13), + 'weekly_portion': "Ha'Azinu", + 'hebrew_weekly_portion': 'האזינו', + 'holiday_name': 'Rosh Hashana II', + 'hebrew_holiday_name': "ב' ראש השנה"}), + make_jerusalem_test_params( + dt(2017, 9, 23, 8, 25), + {'upcoming_candle_lighting': dt(2017, 9, 20, 18, 23), + 'upcoming_havdalah': dt(2017, 9, 23, 19, 13), + 'upcoming_shabbat_candle_lighting': dt(2017, 9, 22, 19, 14), + 'upcoming_shabbat_havdalah': dt(2017, 9, 23, 19, 13), + 'weekly_portion': "Ha'Azinu", + 'hebrew_weekly_portion': 'האזינו', + 'holiday_name': '', + 'hebrew_holiday_name': ''}), + ] + + shabbat_test_ids = [ + "currently_first_shabbat", + "currently_first_shabbat_with_havdalah_offset", + "after_first_shabbat", + "friday_upcoming_shabbat", + "upcoming_rosh_hashana", + "currently_rosh_hashana", + "second_day_rosh_hashana", + "currently_shabbat_chol_hamoed", + "upcoming_two_day_yomtov_in_diaspora", + "currently_first_day_of_two_day_yomtov_in_diaspora", + "currently_second_day_of_two_day_yomtov_in_diaspora", + "upcoming_one_day_yom_tov_in_israel", + "currently_one_day_yom_tov_in_israel", + "after_one_day_yom_tov_in_israel", + # Type 1 = Sat/Sun/Mon + "currently_first_day_of_three_day_type1_yomtov_in_diaspora", + "currently_second_day_of_three_day_type1_yomtov_in_diaspora", + # Type 2 = Thurs/Fri/Sat + "currently_first_day_of_three_day_type2_yomtov_in_israel", + "currently_second_day_of_three_day_type2_yomtov_in_israel", + "currently_third_day_of_three_day_type2_yomtov_in_israel", + ] + + @pytest.mark.parametrize(["now", "candle_lighting", "havdalah", "diaspora", + "tzname", "latitude", "longitude", "result"], + shabbat_params, ids=shabbat_test_ids) + def test_shabbat_times_sensor(self, now, candle_lighting, havdalah, + diaspora, tzname, latitude, longitude, + result): + """Test sensor output for upcoming shabbat/yomtov times.""" + time_zone = get_time_zone(tzname) + set_default_time_zone(time_zone) + test_time = time_zone.localize(now) + for sensor_type, value in result.items(): + if isinstance(value, dt): + result[sensor_type] = time_zone.localize(value) + self.hass.config.latitude = latitude + self.hass.config.longitude = longitude + + if ('upcoming_shabbat_candle_lighting' in result + and 'upcoming_candle_lighting' not in result): + result['upcoming_candle_lighting'] = \ + result['upcoming_shabbat_candle_lighting'] + if ('upcoming_shabbat_havdalah' in result + and 'upcoming_havdalah' not in result): + result['upcoming_havdalah'] = result['upcoming_shabbat_havdalah'] + + for sensor_type, result_value in result.items(): + language = 'english' + if sensor_type.startswith('hebrew_'): + language = 'hebrew' + sensor_type = sensor_type.replace('hebrew_', '') + sensor = JewishCalSensor( + name='test', language=language, sensor_type=sensor_type, + latitude=latitude, longitude=longitude, + timezone=time_zone, diaspora=diaspora, + havdalah_offset=havdalah, + candle_lighting_offset=candle_lighting) + sensor.hass = self.hass + with patch('homeassistant.util.dt.now', return_value=test_time): + run_coroutine_threadsafe( + sensor.async_update(), + self.hass.loop).result() + assert sensor.state == result_value, "Value for {}".format( + sensor_type) + + melacha_params = [ + make_nyc_test_params(dt(2018, 9, 1, 16, 0), True), + make_nyc_test_params(dt(2018, 9, 1, 20, 21), False), + make_nyc_test_params(dt(2018, 9, 7, 13, 1), False), + make_nyc_test_params(dt(2018, 9, 8, 21, 25), False), + make_nyc_test_params(dt(2018, 9, 9, 21, 25), True), + make_nyc_test_params(dt(2018, 9, 10, 21, 25), True), + make_nyc_test_params(dt(2018, 9, 28, 21, 25), True), + make_nyc_test_params(dt(2018, 9, 29, 21, 25), False), + make_nyc_test_params(dt(2018, 9, 30, 21, 25), True), + make_nyc_test_params(dt(2018, 10, 1, 21, 25), True), + make_jerusalem_test_params(dt(2018, 9, 29, 21, 25), False), + make_jerusalem_test_params(dt(2018, 9, 30, 21, 25), True), + make_jerusalem_test_params(dt(2018, 10, 1, 21, 25), False), + ] + melacha_test_ids = [ + "currently_first_shabbat", + "after_first_shabbat", + "friday_upcoming_shabbat", + "upcoming_rosh_hashana", + "currently_rosh_hashana", + "second_day_rosh_hashana", + "currently_shabbat_chol_hamoed", + "upcoming_two_day_yomtov_in_diaspora", + "currently_first_day_of_two_day_yomtov_in_diaspora", + "currently_second_day_of_two_day_yomtov_in_diaspora", + "upcoming_one_day_yom_tov_in_israel", + "currently_one_day_yom_tov_in_israel", + "after_one_day_yom_tov_in_israel", + ] + + @pytest.mark.parametrize(["now", "candle_lighting", "havdalah", "diaspora", + "tzname", "latitude", "longitude", "result"], + melacha_params, ids=melacha_test_ids) + def test_issur_melacha_sensor(self, now, candle_lighting, havdalah, + diaspora, tzname, latitude, longitude, + result): + """Test Issur Melacha sensor output.""" + time_zone = get_time_zone(tzname) + set_default_time_zone(time_zone) + test_time = time_zone.localize(now) + self.hass.config.latitude = latitude + self.hass.config.longitude = longitude + sensor = JewishCalSensor( + name='test', language='english', + sensor_type='issur_melacha_in_effect', + latitude=latitude, longitude=longitude, + timezone=time_zone, diaspora=diaspora, havdalah_offset=havdalah, + candle_lighting_offset=candle_lighting) sensor.hass = self.hass with patch('homeassistant.util.dt.now', return_value=test_time): run_coroutine_threadsafe( From 96d20a64d5cc6575f3a90cd53589c8d3d3a351ba Mon Sep 17 00:00:00 2001 From: SNoof85 Date: Fri, 11 Jan 2019 02:13:29 +0100 Subject: [PATCH 082/235] add_entities -> async_add_entities (#19943) Better state handling at HA startup --- homeassistant/components/sensor/freebox.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sensor/freebox.py b/homeassistant/components/sensor/freebox.py index cc737d2d398..2f8ccbc745d 100644 --- a/homeassistant/components/sensor/freebox.py +++ b/homeassistant/components/sensor/freebox.py @@ -15,13 +15,13 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_platform( - hass, config, add_entities, discovery_info=None): + hass, config, async_add_entities, discovery_info=None): """Set up the sensors.""" fbx = hass.data[DATA_FREEBOX] - add_entities([ + async_add_entities([ FbxRXSensor(fbx), FbxTXSensor(fbx) - ]) + ], True) class FbxSensor(Entity): From 47f64b472dc3d03346f99dc9ceb06e3b9dfd8844 Mon Sep 17 00:00:00 2001 From: Sergey Rymsha Date: Fri, 11 Jan 2019 02:21:57 +0100 Subject: [PATCH 083/235] Add nad telnet (#19704) * fix cla-bot * fix bug introduced after linter complaint * merge two components into one support telnet port configuration * remove obsolete nadtcp component. nad component must be used instead. * back to correct nad_receiver version --- homeassistant/components/media_player/nad.py | 262 ++++++++++++++---- .../components/media_player/nadtcp.py | 178 ------------ requirements_all.txt | 3 +- 3 files changed, 212 insertions(+), 231 deletions(-) delete mode 100644 homeassistant/components/media_player/nadtcp.py diff --git a/homeassistant/components/media_player/nad.py b/homeassistant/components/media_player/nad.py index 5fff8831617..57dca116f6b 100644 --- a/homeassistant/components/media_player/nad.py +++ b/homeassistant/components/media_player/nad.py @@ -8,53 +8,87 @@ import logging import voluptuous as vol +import homeassistant.helpers.config_validation as cv from homeassistant.components.media_player import ( PLATFORM_SCHEMA, SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP, MediaPlayerDevice) from homeassistant.const import CONF_NAME, STATE_OFF, STATE_ON -import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['nad_receiver==0.0.9'] +REQUIREMENTS = ['nad_receiver==0.0.11'] _LOGGER = logging.getLogger(__name__) +DEFAULT_TYPE = 'RS232' +DEFAULT_SERIAL_PORT = '/dev/ttyUSB0' +DEFAULT_PORT = 53 DEFAULT_NAME = 'NAD Receiver' DEFAULT_MIN_VOLUME = -92 DEFAULT_MAX_VOLUME = -20 +DEFAULT_VOLUME_STEP = 4 SUPPORT_NAD = SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ - SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_VOLUME_STEP | \ - SUPPORT_SELECT_SOURCE + SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_VOLUME_STEP | \ + SUPPORT_SELECT_SOURCE -CONF_SERIAL_PORT = 'serial_port' +CONF_TYPE = 'type' +CONF_SERIAL_PORT = 'serial_port' # for NADReceiver +CONF_HOST = 'host' # for NADReceiverTelnet +CONF_PORT = 'port' # for NADReceiverTelnet CONF_MIN_VOLUME = 'min_volume' CONF_MAX_VOLUME = 'max_volume' -CONF_SOURCE_DICT = 'sources' +CONF_VOLUME_STEP = 'volume_step' # for NADReceiverTCP +CONF_SOURCE_DICT = 'sources' # for NADReceiver SOURCE_DICT_SCHEMA = vol.Schema({ vol.Range(min=1, max=10): cv.string }) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_SERIAL_PORT): cv.string, + vol.Optional(CONF_TYPE, default=DEFAULT_TYPE): + vol.In(['RS232', 'Telnet', 'TCP']), + vol.Optional(CONF_SERIAL_PORT, default=DEFAULT_SERIAL_PORT): + cv.string, + vol.Optional(CONF_HOST): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): int, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_MIN_VOLUME, default=DEFAULT_MIN_VOLUME): int, vol.Optional(CONF_MAX_VOLUME, default=DEFAULT_MAX_VOLUME): int, vol.Optional(CONF_SOURCE_DICT, default={}): SOURCE_DICT_SCHEMA, + vol.Optional(CONF_VOLUME_STEP, default=DEFAULT_VOLUME_STEP): int, }) def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the NAD platform.""" - from nad_receiver import NADReceiver - add_entities([NAD( - config.get(CONF_NAME), - NADReceiver(config.get(CONF_SERIAL_PORT)), - config.get(CONF_MIN_VOLUME), - config.get(CONF_MAX_VOLUME), - config.get(CONF_SOURCE_DICT) - )], True) + if config.get(CONF_TYPE) == 'RS232': + from nad_receiver import NADReceiver + add_entities([NAD( + config.get(CONF_NAME), + NADReceiver(config.get(CONF_SERIAL_PORT)), + config.get(CONF_MIN_VOLUME), + config.get(CONF_MAX_VOLUME), + config.get(CONF_SOURCE_DICT) + )], True) + elif config.get(CONF_TYPE) == 'Telnet': + from nad_receiver import NADReceiverTelnet + add_entities([NAD( + config.get(CONF_NAME), + NADReceiverTelnet(config.get(CONF_HOST), + config.get(CONF_PORT)), + config.get(CONF_MIN_VOLUME), + config.get(CONF_MAX_VOLUME), + config.get(CONF_SOURCE_DICT) + )], True) + else: + from nad_receiver import NADReceiverTCP + add_entities([NADtcp( + config.get(CONF_NAME), + NADReceiverTCP(config.get(CONF_HOST)), + config.get(CONF_MIN_VOLUME), + config.get(CONF_MAX_VOLUME), + config.get(CONF_VOLUME_STEP), + )], True) class NAD(MediaPlayerDevice): @@ -73,24 +107,6 @@ class NAD(MediaPlayerDevice): self._volume = self._state = self._mute = self._source = None - def calc_volume(self, decibel): - """ - Calculate the volume given the decibel. - - Return the volume (0..1). - """ - return abs(self._min_volume - decibel) / abs( - self._min_volume - self._max_volume) - - def calc_db(self, volume): - """ - Calculate the decibel given the volume. - - Return the dB. - """ - return self._min_volume + round( - abs(self._min_volume - self._max_volume) * volume) - @property def name(self): """Return the name of the device.""" @@ -101,22 +117,6 @@ class NAD(MediaPlayerDevice): """Return the state of the device.""" return self._state - def update(self): - """Retrieve latest state.""" - if self._nad_receiver.main_power('?') == 'Off': - self._state = STATE_OFF - else: - self._state = STATE_ON - - if self._nad_receiver.main_mute('?') == 'Off': - self._mute = False - else: - self._mute = True - - self._volume = self.calc_volume(self._nad_receiver.main_volume('?')) - self._source = self._source_dict.get( - self._nad_receiver.main_source('?')) - @property def volume_level(self): """Volume level of the media player (0..1).""" @@ -152,6 +152,13 @@ class NAD(MediaPlayerDevice): """Set volume level, range 0..1.""" self._nad_receiver.main_volume('=', self.calc_db(volume)) + def mute_volume(self, mute): + """Mute (true) or unmute (false) media player.""" + if mute: + self._nad_receiver.main_mute('=', 'On') + else: + self._nad_receiver.main_mute('=', 'Off') + def select_source(self, source): """Select input source.""" self._nad_receiver.main_source('=', self._reverse_mapping.get(source)) @@ -166,9 +173,162 @@ class NAD(MediaPlayerDevice): """List of available input sources.""" return sorted(list(self._reverse_mapping.keys())) + def update(self): + """Retrieve latest state.""" + if self._nad_receiver.main_power('?') == 'Off': + self._state = STATE_OFF + else: + self._state = STATE_ON + + if self._nad_receiver.main_mute('?') == 'Off': + self._mute = False + else: + self._mute = True + + self._volume = self.calc_volume(self._nad_receiver.main_volume('?')) + self._source = self._source_dict.get( + self._nad_receiver.main_source('?')) + + def calc_volume(self, decibel): + """ + Calculate the volume given the decibel. + + Return the volume (0..1). + """ + return abs(self._min_volume - decibel) / abs( + self._min_volume - self._max_volume) + + def calc_db(self, volume): + """ + Calculate the decibel given the volume. + + Return the dB. + """ + return self._min_volume + round( + abs(self._min_volume - self._max_volume) * volume) + + +class NADtcp(MediaPlayerDevice): + """Representation of a NAD Digital amplifier.""" + + def __init__(self, name, nad_device, min_volume, max_volume, volume_step): + """Initialize the amplifier.""" + self._name = name + self._nad_receiver = nad_device + self._min_vol = (min_volume + 90) * 2 # from dB to nad vol (0-200) + self._max_vol = (max_volume + 90) * 2 # from dB to nad vol (0-200) + self._volume_step = volume_step + self._state = None + self._mute = None + self._nad_volume = None + self._volume = None + self._source = None + self._source_list = self._nad_receiver.available_sources() + + @property + def name(self): + """Return the name of the device.""" + return self._name + + @property + def state(self): + """Return the state of the device.""" + return self._state + + @property + def volume_level(self): + """Volume level of the media player (0..1).""" + return self._volume + + @property + def is_volume_muted(self): + """Boolean if volume is currently muted.""" + return self._mute + + @property + def supported_features(self): + """Flag media player features that are supported.""" + return SUPPORT_NAD + + def turn_off(self): + """Turn the media player off.""" + self._nad_receiver.power_off() + + def turn_on(self): + """Turn the media player on.""" + self._nad_receiver.power_on() + + def volume_up(self): + """Step volume up in the configured increments.""" + self._nad_receiver.set_volume(self._nad_volume + 2 * self._volume_step) + + def volume_down(self): + """Step volume down in the configured increments.""" + self._nad_receiver.set_volume(self._nad_volume - 2 * self._volume_step) + + def set_volume_level(self, volume): + """Set volume level, range 0..1.""" + nad_volume_to_set = \ + int(round(volume * (self._max_vol - self._min_vol) + + self._min_vol)) + self._nad_receiver.set_volume(nad_volume_to_set) + def mute_volume(self, mute): """Mute (true) or unmute (false) media player.""" if mute: - self._nad_receiver.main_mute('=', 'On') + self._nad_receiver.mute() else: - self._nad_receiver.main_mute('=', 'Off') + self._nad_receiver.unmute() + + def select_source(self, source): + """Select input source.""" + self._nad_receiver.select_source(source) + + @property + def source(self): + """Name of the current input source.""" + return self._source + + @property + def source_list(self): + """List of available input sources.""" + return self._nad_receiver.available_sources() + + def update(self): + """Get the latest details from the device.""" + try: + nad_status = self._nad_receiver.status() + except OSError: + return + if nad_status is None: + return + + # Update on/off state + if nad_status['power']: + self._state = STATE_ON + else: + self._state = STATE_OFF + + # Update current volume + self._volume = self.nad_vol_to_internal_vol(nad_status['volume']) + self._nad_volume = nad_status['volume'] + + # Update muted state + self._mute = nad_status['muted'] + + # Update current source + self._source = nad_status['source'] + + def nad_vol_to_internal_vol(self, nad_volume): + """Convert nad volume range (0-200) to internal volume range. + + Takes into account configured min and max volume. + """ + if nad_volume < self._min_vol: + volume_internal = 0.0 + elif nad_volume > self._max_vol: + volume_internal = 1.0 + else: + volume_internal = (nad_volume - self._min_vol) / \ + (self._max_vol - self._min_vol) + return volume_internal diff --git a/homeassistant/components/media_player/nadtcp.py b/homeassistant/components/media_player/nadtcp.py deleted file mode 100644 index dd3c5e26d7a..00000000000 --- a/homeassistant/components/media_player/nadtcp.py +++ /dev/null @@ -1,178 +0,0 @@ -""" -Support for NAD digital amplifiers which can be remote controlled via tcp/ip. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/media_player.nadtcp/ -""" -import logging - -import voluptuous as vol - -from homeassistant.components.media_player import ( - PLATFORM_SCHEMA, SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, - SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP, - MediaPlayerDevice) -from homeassistant.const import CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON -import homeassistant.helpers.config_validation as cv - -REQUIREMENTS = ['nad_receiver==0.0.9'] - -_LOGGER = logging.getLogger(__name__) - -DEFAULT_NAME = 'NAD amplifier' -DEFAULT_MIN_VOLUME = -60 -DEFAULT_MAX_VOLUME = -10 -DEFAULT_VOLUME_STEP = 4 - -SUPPORT_NAD = SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | SUPPORT_TURN_ON | \ - SUPPORT_TURN_OFF | SUPPORT_VOLUME_STEP | SUPPORT_SELECT_SOURCE - -CONF_MIN_VOLUME = 'min_volume' -CONF_MAX_VOLUME = 'max_volume' -CONF_VOLUME_STEP = 'volume_step' - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_MIN_VOLUME, default=DEFAULT_MIN_VOLUME): int, - vol.Optional(CONF_MAX_VOLUME, default=DEFAULT_MAX_VOLUME): int, - vol.Optional(CONF_VOLUME_STEP, default=DEFAULT_VOLUME_STEP): int, -}) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the NAD platform.""" - from nad_receiver import NADReceiverTCP - add_entities([NADtcp( - NADReceiverTCP(config.get(CONF_HOST)), - config.get(CONF_NAME), - config.get(CONF_MIN_VOLUME), - config.get(CONF_MAX_VOLUME), - config.get(CONF_VOLUME_STEP), - )], True) - - -class NADtcp(MediaPlayerDevice): - """Representation of a NAD Digital amplifier.""" - - def __init__(self, nad_device, name, min_volume, max_volume, volume_step): - """Initialize the amplifier.""" - self._name = name - self.nad_device = nad_device - self._min_vol = (min_volume + 90) * 2 # from dB to nad vol (0-200) - self._max_vol = (max_volume + 90) * 2 # from dB to nad vol (0-200) - self._volume_step = volume_step - self._state = None - self._mute = None - self._nad_volume = None - self._volume = None - self._source = None - self._source_list = self.nad_device.available_sources() - - @property - def name(self): - """Return the name of the device.""" - return self._name - - @property - def state(self): - """Return the state of the device.""" - return self._state - - def update(self): - """Get the latest details from the device.""" - try: - nad_status = self.nad_device.status() - except OSError: - return - if nad_status is None: - return - - # Update on/off state - if nad_status['power']: - self._state = STATE_ON - else: - self._state = STATE_OFF - - # Update current volume - self._volume = self.nad_vol_to_internal_vol(nad_status['volume']) - self._nad_volume = nad_status['volume'] - - # Update muted state - self._mute = nad_status['muted'] - - # Update current source - self._source = nad_status['source'] - - def nad_vol_to_internal_vol(self, nad_volume): - """Convert nad volume range (0-200) to internal volume range. - - Takes into account configured min and max volume. - """ - if nad_volume < self._min_vol: - volume_internal = 0.0 - if nad_volume > self._max_vol: - volume_internal = 1.0 - else: - volume_internal = (nad_volume - self._min_vol) / \ - (self._max_vol - self._min_vol) - return volume_internal - - @property - def supported_features(self): - """Flag media player features that are supported.""" - return SUPPORT_NAD - - def turn_off(self): - """Turn the media player off.""" - self.nad_device.power_off() - - def turn_on(self): - """Turn the media player on.""" - self.nad_device.power_on() - - def volume_up(self): - """Step volume up in the configured increments.""" - self.nad_device.set_volume(self._nad_volume + 2 * self._volume_step) - - def volume_down(self): - """Step volume down in the configured increments.""" - self.nad_device.set_volume(self._nad_volume - 2 * self._volume_step) - - def set_volume_level(self, volume): - """Set volume level, range 0..1.""" - nad_volume_to_set = \ - int(round(volume * (self._max_vol - self._min_vol) + - self._min_vol)) - self.nad_device.set_volume(nad_volume_to_set) - - def mute_volume(self, mute): - """Mute (true) or unmute (false) media player.""" - if mute: - self.nad_device.mute() - else: - self.nad_device.unmute() - - def select_source(self, source): - """Select input source.""" - self.nad_device.select_source(source) - - @property - def source(self): - """Name of the current input source.""" - return self._source - - @property - def source_list(self): - """List of available input sources.""" - return self.nad_device.available_sources() - - @property - def volume_level(self): - """Volume level of the media player (0..1).""" - return self._volume - - @property - def is_volume_muted(self): - """Boolean if volume is currently muted.""" - return self._mute diff --git a/requirements_all.txt b/requirements_all.txt index 45884ccf030..944e4a70751 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -689,8 +689,7 @@ mycroftapi==2.0 myusps==1.3.2 # homeassistant.components.media_player.nad -# homeassistant.components.media_player.nadtcp -nad_receiver==0.0.9 +nad_receiver==0.0.11 # homeassistant.components.light.nanoleaf_aurora nanoleaf==0.4.1 From 2be0d1b0969a823bded0b474f121d72a1d703435 Mon Sep 17 00:00:00 2001 From: Kevin Fronczak Date: Thu, 10 Jan 2019 17:24:35 -0800 Subject: [PATCH 084/235] Upgrade blinkpy and use calibrated temperature for sensor (#19723) --- homeassistant/components/blink/__init__.py | 2 +- homeassistant/components/sensor/blink.py | 7 +++++-- requirements_all.txt | 2 +- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/blink/__init__.py b/homeassistant/components/blink/__init__.py index a56885a22a9..ac2a4574b9c 100644 --- a/homeassistant/components/blink/__init__.py +++ b/homeassistant/components/blink/__init__.py @@ -15,7 +15,7 @@ from homeassistant.const import ( CONF_BINARY_SENSORS, CONF_SENSORS, CONF_FILENAME, CONF_MONITORED_CONDITIONS, TEMP_FAHRENHEIT) -REQUIREMENTS = ['blinkpy==0.11.0'] +REQUIREMENTS = ['blinkpy==0.11.1'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sensor/blink.py b/homeassistant/components/sensor/blink.py index 6d3ca87c4ae..a1a07cb2a73 100644 --- a/homeassistant/components/sensor/blink.py +++ b/homeassistant/components/sensor/blink.py @@ -44,6 +44,9 @@ class BlinkSensor(Entity): self._unit_of_measurement = units self._icon = icon self._unique_id = "{}-{}".format(self._camera.serial, self._type) + self._sensor_key = self._type + if self._type == 'temperature': + self._sensor_key = 'temperature_calibrated' @property def name(self): @@ -74,9 +77,9 @@ class BlinkSensor(Entity): """Retrieve sensor data from the camera.""" self.data.refresh() try: - self._state = self._camera.attributes[self._type] + self._state = self._camera.attributes[self._sensor_key] except KeyError: self._state = None _LOGGER.error( "%s not a valid camera attribute. Did the API change?", - self._type) + self._sensor_key) diff --git a/requirements_all.txt b/requirements_all.txt index 944e4a70751..28bccd62ea9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -196,7 +196,7 @@ bellows==0.7.0 bimmer_connected==0.5.3 # homeassistant.components.blink -blinkpy==0.11.0 +blinkpy==0.11.1 # homeassistant.components.light.blinksticklight blinkstick==1.1.8 From ca460ace5d743adbe99e395b279d3a9080880ed3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ab=C3=ADlio=20Costa?= Date: Fri, 11 Jan 2019 01:39:49 +0000 Subject: [PATCH 085/235] Small refactoring for the alexa component (#19782) * small refactoring * fix tests --- homeassistant/components/alexa/__init__.py | 2 +- homeassistant/components/alexa/smart_home.py | 15 ++++++--------- tests/components/alexa/test_smart_home.py | 2 +- 3 files changed, 8 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/alexa/__init__.py b/homeassistant/components/alexa/__init__.py index 0bfa01a83ca..8491268dfd6 100644 --- a/homeassistant/components/alexa/__init__.py +++ b/homeassistant/components/alexa/__init__.py @@ -72,6 +72,6 @@ async def async_setup(hass, config): pass else: smart_home_config = smart_home_config or SMART_HOME_SCHEMA({}) - smart_home.async_setup(hass, smart_home_config) + await smart_home.async_setup(hass, smart_home_config) return True diff --git a/homeassistant/components/alexa/smart_home.py b/homeassistant/components/alexa/smart_home.py index beb1a9c15ee..7240912883a 100644 --- a/homeassistant/components/alexa/smart_home.py +++ b/homeassistant/components/alexa/smart_home.py @@ -1036,8 +1036,7 @@ class Config: self.entity_config = entity_config or {} -@ha.callback -def async_setup(hass, config): +async def async_setup(hass, config): """Activate Smart Home functionality of Alexa component. This is optional, triggered by having a `smart_home:` sub-section in the @@ -1063,8 +1062,7 @@ def async_setup(hass, config): hass.http.register_view(SmartHomeView(smart_home_config)) if AUTH_KEY in hass.data: - hass.loop.create_task( - async_enable_proactive_mode(hass, smart_home_config)) + await async_enable_proactive_mode(hass, smart_home_config) async def async_enable_proactive_mode(hass, smart_home_config): @@ -1380,8 +1378,7 @@ async def async_send_changereport_message(hass, config, alexa_entity): return headers = { - "Authorization": "Bearer {}".format(token), - "Content-Type": "application/json;charset=UTF-8" + "Authorization": "Bearer {}".format(token) } endpoint = alexa_entity.entity_id() @@ -1402,14 +1399,14 @@ async def async_send_changereport_message(hass, config, alexa_entity): payload=payload) message.set_endpoint_full(token, endpoint) - message_str = json.dumps(message.serialize()) + message_serialized = message.serialize() try: session = aiohttp_client.async_get_clientsession(hass) with async_timeout.timeout(DEFAULT_TIMEOUT, loop=hass.loop): response = await session.post(config.endpoint, headers=headers, - data=message_str, + json=message_serialized, allow_redirects=True) except (asyncio.TimeoutError, aiohttp.ClientError): @@ -1418,7 +1415,7 @@ async def async_send_changereport_message(hass, config, alexa_entity): response_text = await response.text() - _LOGGER.debug("Sent: %s", message_str) + _LOGGER.debug("Sent: %s", json.dumps(message_serialized)) _LOGGER.debug("Received (%s): %s", response.status, response_text) if response.status != 202: diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index 536112e9b0b..845e59295ac 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -1783,7 +1783,7 @@ async def test_report_state(hass, aioclient_mock): assert len(aioclient_mock.mock_calls) == 1 call = aioclient_mock.mock_calls - call_json = json.loads(call[0][2]) + call_json = call[0][2] assert call_json["event"]["payload"]["change"]["properties"][0][ "value"] == "NOT_DETECTED" assert call_json["event"]["endpoint"][ From 4c1eeb9e96a43cfb2f8c0455ca67529501379d32 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Fri, 11 Jan 2019 02:43:31 +0100 Subject: [PATCH 086/235] Upgrade pylast to 3.0.0 (#19938) --- homeassistant/components/sensor/lastfm.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/lastfm.py b/homeassistant/components/sensor/lastfm.py index d92fe8b53bf..fa69a916495 100644 --- a/homeassistant/components/sensor/lastfm.py +++ b/homeassistant/components/sensor/lastfm.py @@ -14,7 +14,7 @@ from homeassistant.const import CONF_API_KEY import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['pylast==2.4.0'] +REQUIREMENTS = ['pylast==3.0.0'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 28bccd62ea9..decc73ab5e2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1057,7 +1057,7 @@ pykwb==0.0.8 pylacrosse==0.3.1 # homeassistant.components.sensor.lastfm -pylast==2.4.0 +pylast==3.0.0 # homeassistant.components.sensor.launch_library pylaunches==0.2.0 From cee51ecb2b38943ab03299b95f9e6a63d0768191 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Herv=C3=A9?= <159137+therve@users.noreply.github.com> Date: Fri, 11 Jan 2019 02:45:50 +0100 Subject: [PATCH 087/235] Remove spurious libzwave error (#19928) If a network_key is not configuired, the following error is logged: TypeError: expected bytes, NoneType found Exception ignored in: 'libopenzwave.str_to_cppstr' TypeError: expected bytes, NoneType found We don't need to set the key if it's None, let's skip in that case. --- homeassistant/components/zwave/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/zwave/__init__.py b/homeassistant/components/zwave/__init__.py index 7860b545be2..093e6071bb4 100644 --- a/homeassistant/components/zwave/__init__.py +++ b/homeassistant/components/zwave/__init__.py @@ -284,7 +284,7 @@ async def async_setup_entry(hass, config_entry): options.set_console_output(use_debug) - if CONF_NETWORK_KEY in config_entry.data: + if config_entry.data.get(CONF_NETWORK_KEY): options.addOption("NetworkKey", config_entry.data[CONF_NETWORK_KEY]) await hass.async_add_executor_job(options.lock) From 31d92683f7fab98c48830e6a27b9131572a60c41 Mon Sep 17 00:00:00 2001 From: mindigmarton Date: Fri, 11 Jan 2019 03:20:35 +0100 Subject: [PATCH 088/235] Add emulated_roku component (#17596) * Add emulated_roku component * Add emulated_roku config tests * Fix emulated_roku test dependencies * Remove emulated_roku yaml support, add tests * Add yaml support, simplify config flow * Improve emulated_roku code quality * Fix emulated_roku translation, improve code quality * Fix emulated_roku translation * Bump emulated_roku to 0.1.6 to fix SSDP discovery * Bump emulated roku to 0.1.7, refactor component start/stop methods --- .../emulated_roku/.translations/en.json | 21 +++ .../components/emulated_roku/__init__.py | 84 ++++++++++ .../components/emulated_roku/binding.py | 147 ++++++++++++++++++ .../components/emulated_roku/config_flow.py | 63 ++++++++ .../components/emulated_roku/const.py | 13 ++ .../components/emulated_roku/strings.json | 21 +++ homeassistant/config_entries.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + script/gen_requirements_all.py | 1 + tests/components/emulated_roku/__init__.py | 1 + .../components/emulated_roku/test_binding.py | 68 ++++++++ .../emulated_roku/test_config_flow.py | 36 +++++ tests/components/emulated_roku/test_init.py | 91 +++++++++++ 14 files changed, 553 insertions(+) create mode 100644 homeassistant/components/emulated_roku/.translations/en.json create mode 100644 homeassistant/components/emulated_roku/__init__.py create mode 100644 homeassistant/components/emulated_roku/binding.py create mode 100644 homeassistant/components/emulated_roku/config_flow.py create mode 100644 homeassistant/components/emulated_roku/const.py create mode 100644 homeassistant/components/emulated_roku/strings.json create mode 100644 tests/components/emulated_roku/__init__.py create mode 100644 tests/components/emulated_roku/test_binding.py create mode 100644 tests/components/emulated_roku/test_config_flow.py create mode 100644 tests/components/emulated_roku/test_init.py diff --git a/homeassistant/components/emulated_roku/.translations/en.json b/homeassistant/components/emulated_roku/.translations/en.json new file mode 100644 index 00000000000..376252966a3 --- /dev/null +++ b/homeassistant/components/emulated_roku/.translations/en.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "name_exists": "Name already exists" + }, + "step": { + "user": { + "data": { + "advertise_ip": "Advertise IP", + "advertise_port": "Advertise port", + "host_ip": "Host IP", + "listen_port": "Listen port", + "name": "Name", + "upnp_bind_multicast": "Bind multicast (True/False)" + }, + "title": "Define server configuration" + } + }, + "title": "EmulatedRoku" + } +} \ No newline at end of file diff --git a/homeassistant/components/emulated_roku/__init__.py b/homeassistant/components/emulated_roku/__init__.py new file mode 100644 index 00000000000..8ebaa5e4b26 --- /dev/null +++ b/homeassistant/components/emulated_roku/__init__.py @@ -0,0 +1,84 @@ +""" +Support for Roku API emulation. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/emulated_roku/ +""" +import voluptuous as vol + +from homeassistant import config_entries, util +from homeassistant.const import CONF_NAME +import homeassistant.helpers.config_validation as cv + +from .binding import EmulatedRoku +from .config_flow import configured_servers +from .const import ( + CONF_ADVERTISE_IP, CONF_ADVERTISE_PORT, CONF_HOST_IP, CONF_LISTEN_PORT, + CONF_SERVERS, CONF_UPNP_BIND_MULTICAST, DOMAIN) + +REQUIREMENTS = ['emulated_roku==0.1.7'] + +SERVER_CONFIG_SCHEMA = vol.Schema({ + vol.Required(CONF_NAME): cv.string, + vol.Required(CONF_LISTEN_PORT): cv.port, + vol.Optional(CONF_HOST_IP): cv.string, + vol.Optional(CONF_ADVERTISE_IP): cv.string, + vol.Optional(CONF_ADVERTISE_PORT): cv.port, + vol.Optional(CONF_UPNP_BIND_MULTICAST): cv.boolean +}) + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_SERVERS): + vol.All(cv.ensure_list, [SERVER_CONFIG_SCHEMA]), + }), +}, extra=vol.ALLOW_EXTRA) + + +async def async_setup(hass, config): + """Set up the emulated roku component.""" + conf = config.get(DOMAIN) + + if conf is None: + return True + + existing_servers = configured_servers(hass) + + for entry in conf[CONF_SERVERS]: + if entry[CONF_NAME] not in existing_servers: + hass.async_create_task(hass.config_entries.flow.async_init( + DOMAIN, + context={'source': config_entries.SOURCE_IMPORT}, + data=entry + )) + + return True + + +async def async_setup_entry(hass, config_entry): + """Set up an emulated roku server from a config entry.""" + config = config_entry.data + + if DOMAIN not in hass.data: + hass.data[DOMAIN] = {} + + name = config[CONF_NAME] + listen_port = config[CONF_LISTEN_PORT] + host_ip = config.get(CONF_HOST_IP) or util.get_local_ip() + advertise_ip = config.get(CONF_ADVERTISE_IP) + advertise_port = config.get(CONF_ADVERTISE_PORT) + upnp_bind_multicast = config.get(CONF_UPNP_BIND_MULTICAST) + + server = EmulatedRoku(hass, name, host_ip, listen_port, + advertise_ip, advertise_port, upnp_bind_multicast) + + hass.data[DOMAIN][name] = server + + return await server.setup() + + +async def async_unload_entry(hass, entry): + """Unload a config entry.""" + name = entry.data[CONF_NAME] + server = hass.data[DOMAIN].pop(name) + return await server.unload() diff --git a/homeassistant/components/emulated_roku/binding.py b/homeassistant/components/emulated_roku/binding.py new file mode 100644 index 00000000000..cd42560288d --- /dev/null +++ b/homeassistant/components/emulated_roku/binding.py @@ -0,0 +1,147 @@ +"""Bridge between emulated_roku and Home Assistant.""" +import logging + +from homeassistant.const import ( + EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) +from homeassistant.core import CoreState, EventOrigin + +LOGGER = logging.getLogger('homeassistant.components.emulated_roku') + +EVENT_ROKU_COMMAND = 'roku_command' + +ATTR_COMMAND_TYPE = 'type' +ATTR_SOURCE_NAME = 'source_name' +ATTR_KEY = 'key' +ATTR_APP_ID = 'app_id' + +ROKU_COMMAND_KEYDOWN = 'keydown' +ROKU_COMMAND_KEYUP = 'keyup' +ROKU_COMMAND_KEYPRESS = 'keypress' +ROKU_COMMAND_LAUNCH = 'launch' + + +class EmulatedRoku: + """Manages an emulated_roku server.""" + + def __init__(self, hass, name, host_ip, listen_port, + advertise_ip, advertise_port, upnp_bind_multicast): + """Initialize the properties.""" + self.hass = hass + + self.roku_usn = name + self.host_ip = host_ip + self.listen_port = listen_port + + self.advertise_port = advertise_port + self.advertise_ip = advertise_ip + + self.bind_multicast = upnp_bind_multicast + + self._api_server = None + + self._unsub_start_listener = None + self._unsub_stop_listener = None + + async def setup(self): + """Start the emulated_roku server.""" + from emulated_roku import EmulatedRokuServer, \ + EmulatedRokuCommandHandler + + class EventCommandHandler(EmulatedRokuCommandHandler): + """emulated_roku command handler to turn commands into events.""" + + def __init__(self, hass): + self.hass = hass + + def on_keydown(self, roku_usn, key): + """Handle keydown event.""" + self.hass.bus.async_fire(EVENT_ROKU_COMMAND, { + ATTR_SOURCE_NAME: roku_usn, + ATTR_COMMAND_TYPE: ROKU_COMMAND_KEYDOWN, + ATTR_KEY: key + }, EventOrigin.local) + + def on_keyup(self, roku_usn, key): + """Handle keyup event.""" + self.hass.bus.async_fire(EVENT_ROKU_COMMAND, { + ATTR_SOURCE_NAME: roku_usn, + ATTR_COMMAND_TYPE: ROKU_COMMAND_KEYUP, + ATTR_KEY: key + }, EventOrigin.local) + + def on_keypress(self, roku_usn, key): + """Handle keypress event.""" + self.hass.bus.async_fire(EVENT_ROKU_COMMAND, { + ATTR_SOURCE_NAME: roku_usn, + ATTR_COMMAND_TYPE: ROKU_COMMAND_KEYPRESS, + ATTR_KEY: key + }, EventOrigin.local) + + def launch(self, roku_usn, app_id): + """Handle launch event.""" + self.hass.bus.async_fire(EVENT_ROKU_COMMAND, { + ATTR_SOURCE_NAME: roku_usn, + ATTR_COMMAND_TYPE: ROKU_COMMAND_LAUNCH, + ATTR_APP_ID: app_id + }, EventOrigin.local) + + LOGGER.debug("Intializing emulated_roku %s on %s:%s", + self.roku_usn, self.host_ip, self.listen_port) + + handler = EventCommandHandler(self.hass) + + self._api_server = EmulatedRokuServer( + self.hass.loop, handler, + self.roku_usn, self.host_ip, self.listen_port, + advertise_ip=self.advertise_ip, + advertise_port=self.advertise_port, + bind_multicast=self.bind_multicast + ) + + async def emulated_roku_stop(event): + """Wrap the call to emulated_roku.close.""" + LOGGER.debug("Stopping emulated_roku %s", self.roku_usn) + self._unsub_stop_listener = None + await self._api_server.close() + + async def emulated_roku_start(event): + """Wrap the call to emulated_roku.start.""" + try: + LOGGER.debug("Starting emulated_roku %s", self.roku_usn) + self._unsub_start_listener = None + await self._api_server.start() + except OSError: + LOGGER.exception("Failed to start Emulated Roku %s on %s:%s", + self.roku_usn, self.host_ip, self.listen_port) + # clean up inconsistent state on errors + await emulated_roku_stop(None) + else: + self._unsub_stop_listener = self.hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, + emulated_roku_stop) + + # start immediately if already running + if self.hass.state == CoreState.running: + await emulated_roku_start(None) + else: + self._unsub_start_listener = self.hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_START, + emulated_roku_start) + + return True + + async def unload(self): + """Unload the emulated_roku server.""" + LOGGER.debug("Unloading emulated_roku %s", self.roku_usn) + + if self._unsub_start_listener: + self._unsub_start_listener() + self._unsub_start_listener = None + + if self._unsub_stop_listener: + self._unsub_stop_listener() + self._unsub_stop_listener = None + + await self._api_server.close() + + return True diff --git a/homeassistant/components/emulated_roku/config_flow.py b/homeassistant/components/emulated_roku/config_flow.py new file mode 100644 index 00000000000..f2d56f84681 --- /dev/null +++ b/homeassistant/components/emulated_roku/config_flow.py @@ -0,0 +1,63 @@ +"""Config flow to configure emulated_roku component.""" + +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_NAME +from homeassistant.core import callback + +from .const import CONF_LISTEN_PORT, DEFAULT_NAME, DEFAULT_PORT, DOMAIN + + +@callback +def configured_servers(hass): + """Return a set of the configured servers.""" + return set(entry.data[CONF_NAME] for entry + in hass.config_entries.async_entries(DOMAIN)) + + +@config_entries.HANDLERS.register(DOMAIN) +class EmulatedRokuFlowHandler(config_entries.ConfigFlow): + """Handle an emulated_roku config flow.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH + + async def async_step_user(self, user_input=None): + """Handle a flow initialized by the user.""" + errors = {} + + if user_input is not None: + name = user_input[CONF_NAME] + + if name in configured_servers(self.hass): + return self.async_abort(reason='name_exists') + + return self.async_create_entry( + title=name, + data=user_input + ) + + servers_num = len(configured_servers(self.hass)) + + if servers_num: + default_name = "{} {}".format(DEFAULT_NAME, servers_num + 1) + default_port = DEFAULT_PORT + servers_num + else: + default_name = DEFAULT_NAME + default_port = DEFAULT_PORT + + return self.async_show_form( + step_id='user', + data_schema=vol.Schema({ + vol.Required(CONF_NAME, + default=default_name): str, + vol.Required(CONF_LISTEN_PORT, + default=default_port): vol.Coerce(int) + }), + errors=errors + ) + + async def async_step_import(self, import_config): + """Handle a flow import.""" + return await self.async_step_user(import_config) diff --git a/homeassistant/components/emulated_roku/const.py b/homeassistant/components/emulated_roku/const.py new file mode 100644 index 00000000000..f4a034e31ac --- /dev/null +++ b/homeassistant/components/emulated_roku/const.py @@ -0,0 +1,13 @@ +"""Constants for the emulated_roku component.""" + +DOMAIN = 'emulated_roku' + +CONF_SERVERS = 'servers' +CONF_LISTEN_PORT = 'listen_port' +CONF_HOST_IP = 'host_ip' +CONF_ADVERTISE_IP = 'advertise_ip' +CONF_ADVERTISE_PORT = 'advertise_port' +CONF_UPNP_BIND_MULTICAST = 'upnp_bind_multicast' + +DEFAULT_NAME = "Home Assistant" +DEFAULT_PORT = 8060 diff --git a/homeassistant/components/emulated_roku/strings.json b/homeassistant/components/emulated_roku/strings.json new file mode 100644 index 00000000000..376252966a3 --- /dev/null +++ b/homeassistant/components/emulated_roku/strings.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "name_exists": "Name already exists" + }, + "step": { + "user": { + "data": { + "advertise_ip": "Advertise IP", + "advertise_port": "Advertise port", + "host_ip": "Host IP", + "listen_port": "Listen port", + "name": "Name", + "upnp_bind_multicast": "Bind multicast (True/False)" + }, + "title": "Define server configuration" + } + }, + "title": "EmulatedRoku" + } +} \ No newline at end of file diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 8af366ce604..dde542e5fc0 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -140,6 +140,7 @@ FLOWS = [ 'deconz', 'dialogflow', 'esphome', + 'emulated_roku', 'geofency', 'hangouts', 'homematicip_cloud', diff --git a/requirements_all.txt b/requirements_all.txt index decc73ab5e2..9ff727b5088 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -351,6 +351,9 @@ eliqonline==1.2.2 # homeassistant.components.elkm1 elkm1-lib==0.7.13 +# homeassistant.components.emulated_roku +emulated_roku==0.1.7 + # homeassistant.components.enocean enocean==0.40 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7886047f4b2..3e60bb511ad 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -61,6 +61,9 @@ defusedxml==0.5.0 # homeassistant.components.sensor.dsmr dsmr_parser==0.12 +# homeassistant.components.emulated_roku +emulated_roku==0.1.7 + # homeassistant.components.sensor.entur_public_transport enturclient==0.1.3 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 5cd1d4b22d1..e351c7b022b 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -46,6 +46,7 @@ TEST_REQUIREMENTS = ( 'coinmarketcap', 'defusedxml', 'dsmr_parser', + 'emulated_roku', 'enturclient', 'ephem', 'evohomeclient', diff --git a/tests/components/emulated_roku/__init__.py b/tests/components/emulated_roku/__init__.py new file mode 100644 index 00000000000..8f50effe0c3 --- /dev/null +++ b/tests/components/emulated_roku/__init__.py @@ -0,0 +1 @@ +"""Tests for emulated_roku.""" diff --git a/tests/components/emulated_roku/test_binding.py b/tests/components/emulated_roku/test_binding.py new file mode 100644 index 00000000000..e0fe2e450a2 --- /dev/null +++ b/tests/components/emulated_roku/test_binding.py @@ -0,0 +1,68 @@ +"""Tests for emulated_roku library bindings.""" +from unittest.mock import Mock, patch + +from homeassistant.components.emulated_roku.binding import EmulatedRoku, \ + EVENT_ROKU_COMMAND, \ + ATTR_SOURCE_NAME, ATTR_COMMAND_TYPE, ATTR_KEY, ATTR_APP_ID, \ + ROKU_COMMAND_KEYPRESS, ROKU_COMMAND_KEYDOWN, \ + ROKU_COMMAND_KEYUP, ROKU_COMMAND_LAUNCH + +from tests.common import mock_coro_func + + +async def test_events_fired_properly(hass): + """Test that events are fired correctly.""" + binding = EmulatedRoku(hass, 'Test Emulated Roku', + '1.2.3.4', 8060, + None, None, None) + + events = [] + roku_event_handler = None + + def instantiate(loop, handler, + roku_usn, host_ip, listen_port, + advertise_ip=None, advertise_port=None, + bind_multicast=None): + nonlocal roku_event_handler + roku_event_handler = handler + + return Mock(start=mock_coro_func(), close=mock_coro_func()) + + def listener(event): + events.append(event) + + with patch('emulated_roku.EmulatedRokuServer', instantiate): + hass.bus.async_listen(EVENT_ROKU_COMMAND, listener) + + assert await binding.setup() is True + + assert roku_event_handler is not None + + roku_event_handler.on_keydown('Test Emulated Roku', 'A') + roku_event_handler.on_keyup('Test Emulated Roku', 'A') + roku_event_handler.on_keypress('Test Emulated Roku', 'C') + roku_event_handler.launch('Test Emulated Roku', '1') + + await hass.async_block_till_done() + + assert len(events) == 4 + + assert events[0].event_type == EVENT_ROKU_COMMAND + assert events[0].data[ATTR_COMMAND_TYPE] == ROKU_COMMAND_KEYDOWN + assert events[0].data[ATTR_SOURCE_NAME] == 'Test Emulated Roku' + assert events[0].data[ATTR_KEY] == 'A' + + assert events[1].event_type == EVENT_ROKU_COMMAND + assert events[1].data[ATTR_COMMAND_TYPE] == ROKU_COMMAND_KEYUP + assert events[1].data[ATTR_SOURCE_NAME] == 'Test Emulated Roku' + assert events[1].data[ATTR_KEY] == 'A' + + assert events[2].event_type == EVENT_ROKU_COMMAND + assert events[2].data[ATTR_COMMAND_TYPE] == ROKU_COMMAND_KEYPRESS + assert events[2].data[ATTR_SOURCE_NAME] == 'Test Emulated Roku' + assert events[2].data[ATTR_KEY] == 'C' + + assert events[3].event_type == EVENT_ROKU_COMMAND + assert events[3].data[ATTR_COMMAND_TYPE] == ROKU_COMMAND_LAUNCH + assert events[3].data[ATTR_SOURCE_NAME] == 'Test Emulated Roku' + assert events[3].data[ATTR_APP_ID] == '1' diff --git a/tests/components/emulated_roku/test_config_flow.py b/tests/components/emulated_roku/test_config_flow.py new file mode 100644 index 00000000000..4d6156bcb92 --- /dev/null +++ b/tests/components/emulated_roku/test_config_flow.py @@ -0,0 +1,36 @@ +"""Tests for emulated_roku config flow.""" +from homeassistant.components.emulated_roku import config_flow +from tests.common import MockConfigEntry + + +async def test_flow_works(hass): + """Test that config flow works.""" + flow = config_flow.EmulatedRokuFlowHandler() + flow.hass = hass + result = await flow.async_step_user(user_input={ + 'name': 'Emulated Roku Test', + 'listen_port': 8060 + }) + + assert result['type'] == 'create_entry' + assert result['title'] == 'Emulated Roku Test' + assert result['data'] == { + 'name': 'Emulated Roku Test', + 'listen_port': 8060 + } + + +async def test_flow_already_registered_entry(hass): + """Test that config flow doesn't allow existing names.""" + MockConfigEntry(domain='emulated_roku', data={ + 'name': 'Emulated Roku Test', + 'listen_port': 8062 + }).add_to_hass(hass) + flow = config_flow.EmulatedRokuFlowHandler() + flow.hass = hass + + result = await flow.async_step_user(user_input={ + 'name': 'Emulated Roku Test', + 'listen_port': 8062 + }) + assert result['type'] == 'abort' diff --git a/tests/components/emulated_roku/test_init.py b/tests/components/emulated_roku/test_init.py new file mode 100644 index 00000000000..5134d987d75 --- /dev/null +++ b/tests/components/emulated_roku/test_init.py @@ -0,0 +1,91 @@ +"""Test emulated_roku component setup process.""" +from unittest.mock import Mock, patch + +from homeassistant.setup import async_setup_component +from homeassistant.components import emulated_roku + +from tests.common import mock_coro_func + + +async def test_config_required_fields(hass): + """Test that configuration is successful with required fields.""" + with patch.object(emulated_roku, 'configured_servers', return_value=[]), \ + patch('emulated_roku.EmulatedRokuServer', + return_value=Mock(start=mock_coro_func(), + close=mock_coro_func())): + assert await async_setup_component(hass, emulated_roku.DOMAIN, { + emulated_roku.DOMAIN: { + emulated_roku.CONF_SERVERS: [{ + emulated_roku.CONF_NAME: 'Emulated Roku Test', + emulated_roku.CONF_LISTEN_PORT: 8060 + }] + } + }) is True + + +async def test_config_already_registered_not_configured(hass): + """Test that an already registered name causes the entry to be ignored.""" + with patch('emulated_roku.EmulatedRokuServer', + return_value=Mock(start=mock_coro_func(), + close=mock_coro_func())) as instantiate, \ + patch.object(emulated_roku, 'configured_servers', + return_value=['Emulated Roku Test']): + assert await async_setup_component(hass, emulated_roku.DOMAIN, { + emulated_roku.DOMAIN: { + emulated_roku.CONF_SERVERS: [{ + emulated_roku.CONF_NAME: 'Emulated Roku Test', + emulated_roku.CONF_LISTEN_PORT: 8060 + }] + } + }) is True + + assert len(instantiate.mock_calls) == 0 + + +async def test_setup_entry_successful(hass): + """Test setup entry is successful.""" + entry = Mock() + entry.data = { + emulated_roku.CONF_NAME: 'Emulated Roku Test', + emulated_roku.CONF_LISTEN_PORT: 8060, + emulated_roku.CONF_HOST_IP: '1.2.3.5', + emulated_roku.CONF_ADVERTISE_IP: '1.2.3.4', + emulated_roku.CONF_ADVERTISE_PORT: 8071, + emulated_roku.CONF_UPNP_BIND_MULTICAST: False + } + + with patch('emulated_roku.EmulatedRokuServer', + return_value=Mock(start=mock_coro_func(), + close=mock_coro_func())) as instantiate: + assert await emulated_roku.async_setup_entry(hass, entry) is True + + assert len(instantiate.mock_calls) == 1 + assert hass.data[emulated_roku.DOMAIN] + + roku_instance = hass.data[emulated_roku.DOMAIN]['Emulated Roku Test'] + + assert roku_instance.roku_usn == 'Emulated Roku Test' + assert roku_instance.host_ip == '1.2.3.5' + assert roku_instance.listen_port == 8060 + assert roku_instance.advertise_ip == '1.2.3.4' + assert roku_instance.advertise_port == 8071 + assert roku_instance.bind_multicast is False + + +async def test_unload_entry(hass): + """Test being able to unload an entry.""" + entry = Mock() + entry.data = {'name': 'Emulated Roku Test', 'listen_port': 8060} + + with patch('emulated_roku.EmulatedRokuServer', + return_value=Mock(start=mock_coro_func(), + close=mock_coro_func())): + assert await emulated_roku.async_setup_entry(hass, entry) is True + + assert emulated_roku.DOMAIN in hass.data + + await hass.async_block_till_done() + + assert await emulated_roku.async_unload_entry(hass, entry) + + assert len(hass.data[emulated_roku.DOMAIN]) == 0 From c3e9bd1444c1d73fb03c09535af364370a86d915 Mon Sep 17 00:00:00 2001 From: Tyler Page Date: Fri, 11 Jan 2019 07:48:36 +0000 Subject: [PATCH 089/235] Change state() to try/except to catch KeyError (#19935) * Change state() to try/except to catch KeyError When Tautulli is up but Plex is down, the API doesn't return a 'stream_count' key. This causes calls to state() to raise KeyError exceptions. The new code includes a try/except to catch the KeyError and return -1 signifying that the Tautulli API cannot talk to Plex * Update tautulli.py --- homeassistant/components/sensor/tautulli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/tautulli.py b/homeassistant/components/sensor/tautulli.py index aa651a9ab1a..5c48731f7df 100644 --- a/homeassistant/components/sensor/tautulli.py +++ b/homeassistant/components/sensor/tautulli.py @@ -119,7 +119,7 @@ class TautulliSensor(Entity): @property def state(self): """Return the state of the sensor.""" - return self.sessions['stream_count'] + return self.sessions.get('stream_count') @property def icon(self): From 97394df0b9cbe183d4fa91c44861e46708d18459 Mon Sep 17 00:00:00 2001 From: Jason Hu Date: Fri, 11 Jan 2019 00:26:25 -0800 Subject: [PATCH 090/235] Only authenticate request when owner can be found --- homeassistant/components/http/auth.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/http/auth.py b/homeassistant/components/http/auth.py index 6cd211613ce..a515fcd198e 100644 --- a/homeassistant/components/http/auth.py +++ b/homeassistant/components/http/auth.py @@ -92,8 +92,8 @@ def setup_auth(app, trusted_networks, api_password): for user in users: if user.is_owner: request['hass_user'] = user + authenticated = True break - authenticated = True request[KEY_AUTHENTICATED] = authenticated return await handler(request) From 7b81727c695d65360ac44b3e14c0b0f72efe5545 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Fri, 11 Jan 2019 10:38:39 +0100 Subject: [PATCH 091/235] Upgrade mutagen to 1.42.0 (#19956) --- homeassistant/components/tts/__init__.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/tts/__init__.py b/homeassistant/components/tts/__init__.py index 86667782d09..d06d28e8621 100644 --- a/homeassistant/components/tts/__init__.py +++ b/homeassistant/components/tts/__init__.py @@ -29,7 +29,7 @@ from homeassistant.helpers import config_per_platform import homeassistant.helpers.config_validation as cv from homeassistant.setup import async_prepare_setup_platform -REQUIREMENTS = ['mutagen==1.41.1'] +REQUIREMENTS = ['mutagen==1.42.0'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 9ff727b5088..3bcd547263a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -680,7 +680,7 @@ mitemp_bt==0.0.1 motorparts==1.0.2 # homeassistant.components.tts -mutagen==1.41.1 +mutagen==1.42.0 # homeassistant.components.mychevy mychevy==1.2.0 From caf0751be8893b92c9340e367f6e9bf50017e5d3 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Fri, 11 Jan 2019 12:37:15 +0100 Subject: [PATCH 092/235] Upgrade pillow to 5.4.1 (#19958) --- homeassistant/components/camera/proxy.py | 2 +- homeassistant/components/image_processing/tensorflow.py | 2 +- requirements_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/camera/proxy.py b/homeassistant/components/camera/proxy.py index e5a0d672756..943e4986623 100644 --- a/homeassistant/components/camera/proxy.py +++ b/homeassistant/components/camera/proxy.py @@ -18,7 +18,7 @@ from homeassistant.util.async_ import run_coroutine_threadsafe import homeassistant.util.dt as dt_util from homeassistant.components.camera import async_get_still_stream -REQUIREMENTS = ['pillow==5.3.0'] +REQUIREMENTS = ['pillow==5.4.1'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/image_processing/tensorflow.py b/homeassistant/components/image_processing/tensorflow.py index 6172963525e..939b5a821cb 100644 --- a/homeassistant/components/image_processing/tensorflow.py +++ b/homeassistant/components/image_processing/tensorflow.py @@ -20,7 +20,7 @@ from homeassistant.core import split_entity_id from homeassistant.helpers import template import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['numpy==1.15.4', 'pillow==5.3.0', 'protobuf==3.6.1'] +REQUIREMENTS = ['numpy==1.15.4', 'pillow==5.4.1', 'protobuf==3.6.1'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 3bcd547263a..db340291fb4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -790,7 +790,7 @@ pilight==0.1.1 # homeassistant.components.camera.proxy # homeassistant.components.image_processing.tensorflow -pillow==5.3.0 +pillow==5.4.1 # homeassistant.components.dominos pizzapi==0.0.3 From 734d8c52e93a5c18ec79a3feaea94adc420e7d3c Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Fri, 11 Jan 2019 16:44:11 +0100 Subject: [PATCH 093/235] Upgrade ruamel.yaml to 0.15.85 (#19960) --- 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 155a0ec6725..eec2f7dde8c 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -12,7 +12,7 @@ python-slugify==1.2.6 pytz>=2018.07 pyyaml>=3.13,<4 requests==2.21.0 -ruamel.yaml==0.15.81 +ruamel.yaml==0.15.85 voluptuous==0.11.5 voluptuous-serialize==2.0.0 diff --git a/requirements_all.txt b/requirements_all.txt index db340291fb4..471c1d5b370 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -13,7 +13,7 @@ python-slugify==1.2.6 pytz>=2018.07 pyyaml>=3.13,<4 requests==2.21.0 -ruamel.yaml==0.15.81 +ruamel.yaml==0.15.85 voluptuous==0.11.5 voluptuous-serialize==2.0.0 diff --git a/setup.py b/setup.py index 4457f725585..6fa648af781 100755 --- a/setup.py +++ b/setup.py @@ -47,7 +47,7 @@ REQUIRES = [ 'pytz>=2018.07', 'pyyaml>=3.13,<4', 'requests==2.21.0', - 'ruamel.yaml==0.15.81', + 'ruamel.yaml==0.15.85', 'voluptuous==0.11.5', 'voluptuous-serialize==2.0.0', ] From 2228f2ef66f40b81f2bda90003bd2d79e48f1161 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Fri, 11 Jan 2019 16:47:53 +0100 Subject: [PATCH 094/235] Upgrade pysnmp to 4.4.8 (#19961) --- homeassistant/components/device_tracker/snmp.py | 2 +- homeassistant/components/sensor/snmp.py | 2 +- homeassistant/components/switch/snmp.py | 2 +- requirements_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/device_tracker/snmp.py b/homeassistant/components/device_tracker/snmp.py index 40a6f48d889..7c6efc82ef9 100644 --- a/homeassistant/components/device_tracker/snmp.py +++ b/homeassistant/components/device_tracker/snmp.py @@ -14,7 +14,7 @@ from homeassistant.components.device_tracker import ( DOMAIN, PLATFORM_SCHEMA, DeviceScanner) from homeassistant.const import CONF_HOST -REQUIREMENTS = ['pysnmp==4.4.6'] +REQUIREMENTS = ['pysnmp==4.4.8'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sensor/snmp.py b/homeassistant/components/sensor/snmp.py index b9997345c36..3964e44e376 100644 --- a/homeassistant/components/sensor/snmp.py +++ b/homeassistant/components/sensor/snmp.py @@ -16,7 +16,7 @@ from homeassistant.const import ( CONF_HOST, CONF_NAME, CONF_PORT, CONF_UNIT_OF_MEASUREMENT, STATE_UNKNOWN, CONF_USERNAME, CONF_VALUE_TEMPLATE) -REQUIREMENTS = ['pysnmp==4.4.6'] +REQUIREMENTS = ['pysnmp==4.4.8'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/switch/snmp.py b/homeassistant/components/switch/snmp.py index e1da12d317e..0baa129657d 100644 --- a/homeassistant/components/switch/snmp.py +++ b/homeassistant/components/switch/snmp.py @@ -14,7 +14,7 @@ from homeassistant.const import ( CONF_USERNAME) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['pysnmp==4.4.6'] +REQUIREMENTS = ['pysnmp==4.4.8'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 471c1d5b370..974d1171192 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1205,7 +1205,7 @@ pysma==0.3.1 # homeassistant.components.device_tracker.snmp # homeassistant.components.sensor.snmp # homeassistant.components.switch.snmp -pysnmp==4.4.6 +pysnmp==4.4.8 # homeassistant.components.sonos pysonos==0.0.5 From 49cfebd9033ead6bd7c32a458f43def56b62f9fd Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Fri, 11 Jan 2019 16:48:07 +0100 Subject: [PATCH 095/235] Upgrade keyring to 17.1.1 (#19962) --- homeassistant/scripts/keyring.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/scripts/keyring.py b/homeassistant/scripts/keyring.py index 9837ff64cd5..f7fa33aca37 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==17.1.0', 'keyrings.alt==3.1.1'] +REQUIREMENTS = ['keyring==17.1.1', 'keyrings.alt==3.1.1'] def run(args): diff --git a/requirements_all.txt b/requirements_all.txt index 974d1171192..9d39fd7f424 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -573,7 +573,7 @@ jsonrpc-async==0.6 jsonrpc-websocket==0.6 # homeassistant.scripts.keyring -keyring==17.1.0 +keyring==17.1.1 # homeassistant.scripts.keyring keyrings.alt==3.1.1 From 17dce6697f2db05e261ac528fef98fce17316c70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren=20Oldag?= Date: Fri, 11 Jan 2019 18:25:14 +0100 Subject: [PATCH 096/235] Add support for restoring state to rpi_gpio_pwm (#19944) --- .../components/light/rpi_gpio_pwm.py | 29 ++++++++++++++++--- requirements_all.txt | 2 +- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/light/rpi_gpio_pwm.py b/homeassistant/components/light/rpi_gpio_pwm.py index a09666e89cd..a3fe0f6b71e 100644 --- a/homeassistant/components/light/rpi_gpio_pwm.py +++ b/homeassistant/components/light/rpi_gpio_pwm.py @@ -8,14 +8,15 @@ import logging import voluptuous as vol -from homeassistant.const import CONF_NAME, CONF_TYPE +from homeassistant.const import CONF_NAME, CONF_TYPE, STATE_ON from homeassistant.components.light import ( Light, ATTR_BRIGHTNESS, ATTR_HS_COLOR, ATTR_TRANSITION, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_TRANSITION, PLATFORM_SCHEMA) import homeassistant.helpers.config_validation as cv import homeassistant.util.color as color_util +from homeassistant.helpers.restore_state import RestoreEntity -REQUIREMENTS = ['pwmled==1.4.0'] +REQUIREMENTS = ['pwmled==1.4.1'] _LOGGER = logging.getLogger(__name__) @@ -34,6 +35,7 @@ CONF_LED_TYPE_RGB = 'rgb' CONF_LED_TYPE_RGBW = 'rgbw' CONF_LED_TYPES = [CONF_LED_TYPE_SIMPLE, CONF_LED_TYPE_RGB, CONF_LED_TYPE_RGBW] +DEFAULT_BRIGHTNESS = 255 DEFAULT_COLOR = [0, 0] SUPPORT_SIMPLE_LED = (SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION) @@ -95,7 +97,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(leds) -class PwmSimpleLed(Light): +class PwmSimpleLed(Light, RestoreEntity): """Representation of a simple one-color PWM LED.""" def __init__(self, led, name): @@ -103,7 +105,18 @@ class PwmSimpleLed(Light): self._led = led self._name = name self._is_on = False - self._brightness = 255 + self._brightness = DEFAULT_BRIGHTNESS + + async def async_added_to_hass(self): + """Handle entity about to be added to hass event.""" + await super().async_added_to_hass() + last_state = await self.async_get_last_state() + if last_state: + self._is_on = (last_state.state == STATE_ON) + self._brightness = last_state.attributes.get('brightness', + DEFAULT_BRIGHTNESS) + self._led.set(is_on=self._is_on, + brightness=_from_hass_brightness(self._brightness)) @property def should_poll(self): @@ -169,6 +182,14 @@ class PwmRgbLed(PwmSimpleLed): super().__init__(led, name) self._color = DEFAULT_COLOR + async def async_added_to_hass(self): + """Handle entity about to be added to hass event.""" + await super().async_added_to_hass() + last_state = await self.async_get_last_state() + if last_state: + self._color = last_state.attributes.get('hs_color', DEFAULT_COLOR) + self._led.set(color=_from_hass_color(self._color)) + @property def hs_color(self): """Return the color property.""" diff --git a/requirements_all.txt b/requirements_all.txt index 9d39fd7f424..775bf5b6928 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -841,7 +841,7 @@ pushbullet.py==0.11.0 pushetta==1.0.15 # homeassistant.components.light.rpi_gpio_pwm -pwmled==1.4.0 +pwmled==1.4.1 # homeassistant.components.august py-august==0.7.0 From 71eaef8da47d8a5dfb3b8c0d9b4cc40ea3d464c4 Mon Sep 17 00:00:00 2001 From: Eliseo Martelli Date: Fri, 11 Jan 2019 18:51:41 +0100 Subject: [PATCH 097/235] add service type in name (#19980) --- homeassistant/components/sensor/prezzibenzina.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sensor/prezzibenzina.py b/homeassistant/components/sensor/prezzibenzina.py index 972a69ee0d9..171fea53314 100644 --- a/homeassistant/components/sensor/prezzibenzina.py +++ b/homeassistant/components/sensor/prezzibenzina.py @@ -67,7 +67,7 @@ async def async_setup_platform( if types is not None and info['fuel'] not in types: continue dev.append(PrezziBenzinaSensor( - index, client, station, name, info['fuel'])) + index, client, station, name, info['fuel'], info['service'])) async_add_entities(dev, True) @@ -75,13 +75,13 @@ async def async_setup_platform( class PrezziBenzinaSensor(Entity): """Implementation of a PrezziBenzina sensor.""" - def __init__(self, index, client, station, name, ft): + def __init__(self, index, client, station, name, ft, srv): """Initialize the PrezziBenzina sensor.""" self._client = client self._index = index self._data = None self._station = station - self._name = "{} {}".format(name, ft) + self._name = "{} {} {}".format(name, ft, srv) @property def name(self): From fd21d6cc9d4003b2fd5d40979ffee3f1b4f57588 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 17 Dec 2018 11:27:03 +0100 Subject: [PATCH 098/235] Add Hass.io user headers to supervisor proxy --- homeassistant/components/hassio/const.py | 2 ++ homeassistant/components/hassio/http.py | 13 ++++++++++--- tests/components/hassio/conftest.py | 11 ++++++++++- tests/components/hassio/test_http.py | 21 +++++++++++++++++++-- tests/test_util/aiohttp.py | 5 +++++ 5 files changed, 46 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/hassio/const.py b/homeassistant/components/hassio/const.py index c539169ebe3..964f94bfb41 100644 --- a/homeassistant/components/hassio/const.py +++ b/homeassistant/components/hassio/const.py @@ -10,3 +10,5 @@ ATTR_USERNAME = 'username' ATTR_PASSWORD = 'password' X_HASSIO = 'X-HASSIO-KEY' +X_HASS_USER_ID = 'X-HASS-USER-ID' +X_HASS_IS_ADMIN = 'X-HASS-IS-ADMIN' diff --git a/homeassistant/components/hassio/http.py b/homeassistant/components/hassio/http.py index be2806716a7..6b8004f7664 100644 --- a/homeassistant/components/hassio/http.py +++ b/homeassistant/components/hassio/http.py @@ -17,7 +17,7 @@ from aiohttp.web_exceptions import HTTPBadGateway from homeassistant.components.http import KEY_AUTHENTICATED, HomeAssistantView -from .const import X_HASSIO +from .const import X_HASSIO, X_HASS_USER_ID, X_HASS_IS_ADMIN _LOGGER = logging.getLogger(__name__) @@ -75,9 +75,16 @@ class HassIOView(HomeAssistantView): read_timeout = _get_timeout(path) hass = request.app['hass'] + data = None + headers = { + X_HASSIO: os.environ.get('HASSIO_TOKEN', ""), + } + user = request.get('hass_user') + if user is not None: + headers[X_HASS_USER_ID] = request['hass_user'].id + headers[X_HASS_IS_ADMIN] = str(int(request['hass_user'].is_admin)) + try: - data = None - headers = {X_HASSIO: os.environ.get('HASSIO_TOKEN', "")} with async_timeout.timeout(10, loop=hass.loop): data = await request.read() if data: diff --git a/tests/components/hassio/conftest.py b/tests/components/hassio/conftest.py index 435de6d1edf..17a26b4fd2f 100644 --- a/tests/components/hassio/conftest.py +++ b/tests/components/hassio/conftest.py @@ -27,7 +27,7 @@ def hassio_env(): @pytest.fixture -def hassio_client(hassio_env, hass, aiohttp_client, legacy_auth): +def hassio_stubs(hassio_env, hass, hass_client, aioclient_mock): """Create mock hassio http client.""" with patch('homeassistant.components.hassio.HassIO.update_hass_api', Mock(return_value=mock_coro({"result": "ok"}))), \ @@ -40,6 +40,15 @@ def hassio_client(hassio_env, hass, aiohttp_client, legacy_auth): 'api_password': API_PASSWORD } })) + + +@pytest.fixture +def hassio_client(hassio_stubs, hass, hass_client): + yield hass.loop.run_until_complete(hass_client()) + + +@pytest.fixture +def hassio_noauth_client(hassio_stubs, hass, aiohttp_client): yield hass.loop.run_until_complete(aiohttp_client(hass.http.app)) diff --git a/tests/components/hassio/test_http.py b/tests/components/hassio/test_http.py index 07db126312b..8fca88d5fda 100644 --- a/tests/components/hassio/test_http.py +++ b/tests/components/hassio/test_http.py @@ -40,9 +40,9 @@ def test_forward_request(hassio_client): 'build_type', [ 'supervisor/info', 'homeassistant/update', 'host/info' ]) -def test_auth_required_forward_request(hassio_client, build_type): +def test_auth_required_forward_request(hassio_noauth_client, build_type): """Test auth required for normal request.""" - resp = yield from hassio_client.post("/api/hassio/{}".format(build_type)) + resp = yield from hassio_noauth_client.post("/api/hassio/{}".format(build_type)) # Check we got right response assert resp.status == 401 @@ -135,3 +135,20 @@ def test_bad_gateway_when_cannot_find_supervisor(hassio_client): HTTP_HEADER_HA_AUTH: API_PASSWORD }) assert resp.status == 502 + + +async def test_forwarding_user_info(hassio_client, hass_admin_user, + aioclient_mock): + """Test that we forward user info correctly.""" + aioclient_mock.get('http://127.0.0.1/hello') + + resp = await hassio_client.get('/api/hassio/hello') + + # Check we got right response + assert resp.status == 200 + + assert len(aioclient_mock.mock_calls) == 1 + + req_headers = aioclient_mock.mock_calls[0][-1] + req_headers['X-HASS-USER-ID'] == hass_admin_user.id + req_headers['X-HASS-IS-ADMIN'] == '1' diff --git a/tests/test_util/aiohttp.py b/tests/test_util/aiohttp.py index f5bf0b8a4f8..8b3b057bfc0 100644 --- a/tests/test_util/aiohttp.py +++ b/tests/test_util/aiohttp.py @@ -182,6 +182,11 @@ class AiohttpClientMockResponse: """Return yarl of URL.""" return self._url + @property + def content_type(self): + """Return yarl of URL.""" + return self._headers.get('content-type') + @property def content(self): """Return content.""" From 7be197b845595505f0f6f5a80f5e7be92d81332c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 17 Dec 2018 12:11:33 +0100 Subject: [PATCH 099/235] Update test_http.py --- tests/components/hassio/test_http.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/components/hassio/test_http.py b/tests/components/hassio/test_http.py index 8fca88d5fda..3f58c6e697e 100644 --- a/tests/components/hassio/test_http.py +++ b/tests/components/hassio/test_http.py @@ -42,7 +42,8 @@ def test_forward_request(hassio_client): ]) def test_auth_required_forward_request(hassio_noauth_client, build_type): """Test auth required for normal request.""" - resp = yield from hassio_noauth_client.post("/api/hassio/{}".format(build_type)) + resp = yield from hassio_noauth_client.post( + "/api/hassio/{}".format(build_type)) # Check we got right response assert resp.status == 401 From b81260e9128eb30428af67baba560831f22d941c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 11 Jan 2019 10:10:36 -0800 Subject: [PATCH 100/235] Fix tests --- tests/components/hassio/conftest.py | 7 ++++++- tests/components/hassio/test_auth.py | 10 ---------- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/tests/components/hassio/conftest.py b/tests/components/hassio/conftest.py index 17a26b4fd2f..f69be17a9e7 100644 --- a/tests/components/hassio/conftest.py +++ b/tests/components/hassio/conftest.py @@ -44,18 +44,23 @@ def hassio_stubs(hassio_env, hass, hass_client, aioclient_mock): @pytest.fixture def hassio_client(hassio_stubs, hass, hass_client): + """Return a Hass.io HTTP client.""" yield hass.loop.run_until_complete(hass_client()) @pytest.fixture def hassio_noauth_client(hassio_stubs, hass, aiohttp_client): + """Return a Hass.io HTTP client without auth.""" yield hass.loop.run_until_complete(aiohttp_client(hass.http.app)) @pytest.fixture def hassio_handler(hass, aioclient_mock): """Create mock hassio handler.""" - websession = hass.helpers.aiohttp_client.async_get_clientsession() + async def get_client_session(): + return hass.helpers.aiohttp_client.async_get_clientsession() + + websession = hass.loop.run_until_complete(get_client_session()) with patch.dict(os.environ, {'HASSIO_TOKEN': HASSIO_TOKEN}): yield HassIO(hass.loop, websession, "127.0.0.1") diff --git a/tests/components/hassio/test_auth.py b/tests/components/hassio/test_auth.py index fdf3230dedc..44eedde5d1d 100644 --- a/tests/components/hassio/test_auth.py +++ b/tests/components/hassio/test_auth.py @@ -10,8 +10,6 @@ from . import API_PASSWORD async def test_login_success(hass, hassio_client): """Test no auth needed for .""" - await register_auth_provider(hass, {'type': 'homeassistant'}) - with patch('homeassistant.auth.providers.homeassistant.' 'HassAuthProvider.async_validate_login', Mock(return_value=mock_coro())) as mock_login: @@ -34,8 +32,6 @@ async def test_login_success(hass, hassio_client): async def test_login_error(hass, hassio_client): """Test no auth needed for error.""" - await register_auth_provider(hass, {'type': 'homeassistant'}) - with patch('homeassistant.auth.providers.homeassistant.' 'HassAuthProvider.async_validate_login', Mock(side_effect=HomeAssistantError())) as mock_login: @@ -58,8 +54,6 @@ async def test_login_error(hass, hassio_client): async def test_login_no_data(hass, hassio_client): """Test auth with no data -> error.""" - await register_auth_provider(hass, {'type': 'homeassistant'}) - with patch('homeassistant.auth.providers.homeassistant.' 'HassAuthProvider.async_validate_login', Mock(side_effect=HomeAssistantError())) as mock_login: @@ -77,8 +71,6 @@ async def test_login_no_data(hass, hassio_client): async def test_login_no_username(hass, hassio_client): """Test auth with no username in data -> error.""" - await register_auth_provider(hass, {'type': 'homeassistant'}) - with patch('homeassistant.auth.providers.homeassistant.' 'HassAuthProvider.async_validate_login', Mock(side_effect=HomeAssistantError())) as mock_login: @@ -100,8 +92,6 @@ async def test_login_no_username(hass, hassio_client): async def test_login_success_extra(hass, hassio_client): """Test auth with extra data.""" - await register_auth_provider(hass, {'type': 'homeassistant'}) - with patch('homeassistant.auth.providers.homeassistant.' 'HassAuthProvider.async_validate_login', Mock(return_value=mock_coro())) as mock_login: From e2f55a959fc3fe3bffb670c214391f264ed92725 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Fri, 11 Jan 2019 19:44:55 +0100 Subject: [PATCH 101/235] Support next generation of the Xiaomi Mi Smart Plug (chuangmi.plug.m3) (#19972) * Support next generation of the Xiaomi Mi Smart Plug (chuangmi.plug.m3) * Fix indent --- homeassistant/components/switch/xiaomi_miio.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/switch/xiaomi_miio.py b/homeassistant/components/switch/xiaomi_miio.py index 9db13446752..4ead90ca4ec 100644 --- a/homeassistant/components/switch/xiaomi_miio.py +++ b/homeassistant/components/switch/xiaomi_miio.py @@ -37,6 +37,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ 'qmi.powerstrip.v1', 'zimi.powerstrip.v2', 'chuangmi.plug.m1', + 'chuangmi.plug.m3', 'chuangmi.plug.v2', 'chuangmi.plug.v3', 'chuangmi.plug.hmi205', @@ -147,8 +148,8 @@ async def async_setup_platform(hass, config, async_add_entities, device = XiaomiPowerStripSwitch(name, plug, model, unique_id) devices.append(device) hass.data[DATA_KEY][host] = device - elif model in ['chuangmi.plug.m1', 'chuangmi.plug.v2', - 'chuangmi.plug.hmi205']: + elif model in ['chuangmi.plug.m1', 'chuangmi.plug.m3', + 'chuangmi.plug.v2', 'chuangmi.plug.hmi205']: from miio import ChuangmiPlug plug = ChuangmiPlug(host, token, model=model) device = XiaomiPlugGenericSwitch(name, plug, model, unique_id) From b3a08d587691f5e6dadfbc111b7cfc05a5bc0922 Mon Sep 17 00:00:00 2001 From: "Jarle B. Hjortand" Date: Fri, 11 Jan 2019 19:55:55 +0100 Subject: [PATCH 102/235] When tradfri experience communication errors make the lights/devices unavailable. (#19288) --- homeassistant/components/light/tradfri.py | 2 ++ homeassistant/components/switch/tradfri.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/homeassistant/components/light/tradfri.py b/homeassistant/components/light/tradfri.py index a26a2eb828a..50e92f15e3c 100644 --- a/homeassistant/components/light/tradfri.py +++ b/homeassistant/components/light/tradfri.py @@ -339,6 +339,8 @@ class TradfriLight(Light): # pylint: disable=import-error from pytradfri.error import PytradfriError if exc: + self._available = False + self.async_schedule_update_ha_state() _LOGGER.warning("Observation failed for %s", self._name, exc_info=exc) diff --git a/homeassistant/components/switch/tradfri.py b/homeassistant/components/switch/tradfri.py index 74997332b07..b247858b062 100644 --- a/homeassistant/components/switch/tradfri.py +++ b/homeassistant/components/switch/tradfri.py @@ -108,6 +108,8 @@ class TradfriSwitch(SwitchDevice): """Start observation of switch.""" from pytradfri.error import PytradfriError if exc: + self._available = False + self.async_schedule_update_ha_state() _LOGGER.warning("Observation failed for %s", self._name, exc_info=exc) From a8f22287caa7f0ac1ffe33affef0d6c7d8f85542 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 11 Jan 2019 11:30:22 -0800 Subject: [PATCH 103/235] Allow embedded platforms (#19948) * Allow embedded platforms * Fix test --- .../components/{light/hue.py => hue/light.py} | 0 homeassistant/const.py | 2 +- homeassistant/loader.py | 31 ++++++++++++++++--- homeassistant/setup.py | 3 +- .../{light/test_hue.py => hue/test_light.py} | 2 +- tests/test_loader.py | 1 - 6 files changed, 31 insertions(+), 8 deletions(-) rename homeassistant/components/{light/hue.py => hue/light.py} (100%) rename tests/components/{light/test_hue.py => hue/test_light.py} (99%) diff --git a/homeassistant/components/light/hue.py b/homeassistant/components/hue/light.py similarity index 100% rename from homeassistant/components/light/hue.py rename to homeassistant/components/hue/light.py diff --git a/homeassistant/const.py b/homeassistant/const.py index a03b5fe52bc..6d1d72193fc 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -8,7 +8,7 @@ __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) # Format for platforms -PLATFORM_FORMAT = '{}.{}' +PLATFORM_FORMAT = '{domain}.{platform}' # Can be used to specify a catch all when registering state or event listeners. MATCH_ALL = '*' diff --git a/homeassistant/loader.py b/homeassistant/loader.py index 61aacd3b233..cd22a69dab1 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -52,18 +52,43 @@ def set_component(hass, # type: HomeAssistant def get_platform(hass, # type: HomeAssistant - domain: str, platform: str) -> Optional[ModuleType]: + domain: str, platform_name: str) -> Optional[ModuleType]: """Try to load specified platform. Async friendly. """ - return get_component(hass, PLATFORM_FORMAT.format(domain, platform)) + platform = _load_file(hass, PLATFORM_FORMAT.format( + domain=domain, platform=platform_name)) + + if platform is None: + # Turn it around for legacy purposes + platform = _load_file(hass, PLATFORM_FORMAT.format( + domain=platform_name, platform=domain)) + + if platform is None: + _LOGGER.error("Unable to find platform %s", platform_name) + + return platform def get_component(hass, # type: HomeAssistant comp_or_platform: str) -> Optional[ModuleType]: """Try to load specified component. + Async friendly. + """ + comp = _load_file(hass, comp_or_platform) + + if comp is None: + _LOGGER.error("Unable to find component %s", comp_or_platform) + + return comp + + +def _load_file(hass, # type: HomeAssistant + comp_or_platform: str) -> Optional[ModuleType]: + """Try to load specified file. + Looks in config dir first, then built-in components. Only returns it if also found to be valid. Async friendly. @@ -134,8 +159,6 @@ def get_component(hass, # type: HomeAssistant ("Error loading %s. Make sure all " "dependencies are installed"), path) - _LOGGER.error("Unable to find component %s", comp_or_platform) - return None diff --git a/homeassistant/setup.py b/homeassistant/setup.py index cc7c4284f9c..49aae2178fc 100644 --- a/homeassistant/setup.py +++ b/homeassistant/setup.py @@ -190,7 +190,8 @@ async def async_prepare_setup_platform(hass: core.HomeAssistant, config: Dict, This method is a coroutine. """ - platform_path = PLATFORM_FORMAT.format(domain, platform_name) + platform_path = PLATFORM_FORMAT.format(domain=domain, + platform=platform_name) def log_error(msg: str) -> None: """Log helper.""" diff --git a/tests/components/light/test_hue.py b/tests/components/hue/test_light.py similarity index 99% rename from tests/components/light/test_hue.py rename to tests/components/hue/test_light.py index ad4026e7f31..023a3416968 100644 --- a/tests/components/light/test_hue.py +++ b/tests/components/hue/test_light.py @@ -11,7 +11,7 @@ import pytest from homeassistant import config_entries from homeassistant.components import hue -import homeassistant.components.light.hue as hue_light +from homeassistant.components.hue import light as hue_light from homeassistant.util import color _LOGGER = logging.getLogger(__name__) diff --git a/tests/test_loader.py b/tests/test_loader.py index 90d259c860e..5bd273ea16a 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -35,7 +35,6 @@ class TestLoader(unittest.TestCase): def test_get_component(self): """Test if get_component works.""" assert http == loader.get_component(self.hass, 'http') - assert loader.get_component(self.hass, 'light.hue') is not None def test_load_order_component(self): """Test if we can get the proper load order of components.""" From 7be015fcc69e9f9424eea832a5e6a472b1be8536 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Fri, 11 Jan 2019 14:34:29 -0500 Subject: [PATCH 104/235] Add services and helper functions to support a config panel for ZHA (#19664) * reconfigure zha device service add log line to reconfigure service for consistency * add entity functions to support new services * added new services and web socket api and split them into their own module * support manufacturer code logging to debug get safe value for manufacturer * update services.yaml * add comma back * update coveragerc * remove blank line * fix type * api cleanup - review comments * move static method to helpers - review comment * convert reconfigure service to websocket command - review comment * change path * fix attribute --- .coveragerc | 1 + homeassistant/components/zha/__init__.py | 64 ++- homeassistant/components/zha/api.py | 416 ++++++++++++++++++ homeassistant/components/zha/const.py | 15 + .../components/zha/entities/entity.py | 114 ++++- homeassistant/components/zha/helpers.py | 12 +- homeassistant/components/zha/services.yaml | 60 +++ 7 files changed, 637 insertions(+), 45 deletions(-) create mode 100644 homeassistant/components/zha/api.py diff --git a/.coveragerc b/.coveragerc index 3f8d0e6959b..3107af9140a 100644 --- a/.coveragerc +++ b/.coveragerc @@ -426,6 +426,7 @@ omit = homeassistant/components/zha/__init__.py homeassistant/components/zha/const.py homeassistant/components/zha/event.py + homeassistant/components/zha/api.py homeassistant/components/zha/entities/* homeassistant/components/zha/helpers.py homeassistant/components/*/zha.py diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index 5dc8f628581..26d11586a1b 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -12,6 +12,7 @@ import types import voluptuous as vol from homeassistant import config_entries, const as ha_const + from homeassistant.components.zha.entities import ZhaDeviceEntity import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE @@ -22,6 +23,8 @@ from homeassistant.helpers.entity_component import EntityComponent from . import config_flow # noqa # pylint: disable=unused-import from . import const as zha_const from .event import ZhaEvent, ZhaRelayEvent +from . import api +from .helpers import convert_ieee from .const import ( COMPONENTS, CONF_BAUDRATE, CONF_DATABASE, CONF_DEVICE_CONFIG, CONF_RADIO_TYPE, CONF_USB_PATH, DATA_ZHA, DATA_ZHA_BRIDGE_ID, @@ -56,22 +59,6 @@ CONFIG_SCHEMA = vol.Schema({ }) }, extra=vol.ALLOW_EXTRA) -ATTR_DURATION = 'duration' -ATTR_IEEE = 'ieee_address' - -SERVICE_PERMIT = 'permit' -SERVICE_REMOVE = 'remove' -SERVICE_SCHEMAS = { - SERVICE_PERMIT: vol.Schema({ - vol.Optional(ATTR_DURATION, default=60): - vol.All(vol.Coerce(int), vol.Range(1, 254)), - }), - SERVICE_REMOVE: vol.Schema({ - vol.Required(ATTR_IEEE): cv.string, - }), -} - - # Zigbee definitions CENTICELSIUS = 'C-100' @@ -179,25 +166,7 @@ async def async_setup_entry(hass, config_entry): config_entry, component) ) - async def permit(service): - """Allow devices to join this network.""" - duration = service.data.get(ATTR_DURATION) - _LOGGER.info("Permitting joins for %ss", duration) - await application_controller.permit(duration) - - hass.services.async_register(DOMAIN, SERVICE_PERMIT, permit, - schema=SERVICE_SCHEMAS[SERVICE_PERMIT]) - - async def remove(service): - """Remove a node from the network.""" - from bellows.types import EmberEUI64, uint8_t - ieee = service.data.get(ATTR_IEEE) - ieee = EmberEUI64([uint8_t(p, base=16) for p in ieee.split(':')]) - _LOGGER.info("Removing node %s", ieee) - await application_controller.remove(ieee) - - hass.services.async_register(DOMAIN, SERVICE_REMOVE, remove, - schema=SERVICE_SCHEMAS[SERVICE_REMOVE]) + api.async_load_api(hass, application_controller, listener) def zha_shutdown(event): """Close radio.""" @@ -209,8 +178,7 @@ async def async_setup_entry(hass, config_entry): async def async_unload_entry(hass, config_entry): """Unload ZHA config entry.""" - hass.services.async_remove(DOMAIN, SERVICE_PERMIT) - hass.services.async_remove(DOMAIN, SERVICE_REMOVE) + api.async_unload_api(hass) dispatchers = hass.data[DATA_ZHA].get(DATA_ZHA_DISPATCHERS, []) for unsub_dispatcher in dispatchers: @@ -285,6 +253,28 @@ class ApplicationListener: if device.ieee in self._events: self._events.pop(device.ieee) + def get_device_entity(self, ieee_str): + """Return ZHADeviceEntity for given ieee.""" + ieee = convert_ieee(ieee_str) + if ieee in self._device_registry: + entities = self._device_registry[ieee] + entity = next( + ent for ent in entities if isinstance(ent, ZhaDeviceEntity)) + return entity + return None + + def get_entities_for_ieee(self, ieee_str): + """Return list of entities for given ieee.""" + ieee = convert_ieee(ieee_str) + if ieee in self._device_registry: + return self._device_registry[ieee] + return [] + + @property + def device_registry(self) -> str: + """Return devices.""" + return self._device_registry + async def async_device_initialized(self, device, join): """Handle device joined and basic information discovered (async).""" import zigpy.profiles diff --git a/homeassistant/components/zha/api.py b/homeassistant/components/zha/api.py new file mode 100644 index 00000000000..a8aa7a2dbb9 --- /dev/null +++ b/homeassistant/components/zha/api.py @@ -0,0 +1,416 @@ +""" +Web socket API for Zigbee Home Automation devices. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/zha/ +""" + +import logging +import voluptuous as vol + +from homeassistant.components import websocket_api +from homeassistant.components.zha.entities import ZhaDeviceEntity +from homeassistant.const import ATTR_ENTITY_ID +import homeassistant.helpers.config_validation as cv +from .const import ( + DOMAIN, ATTR_CLUSTER_ID, ATTR_CLUSTER_TYPE, ATTR_ATTRIBUTE, ATTR_VALUE, + ATTR_MANUFACTURER, ATTR_COMMAND, ATTR_COMMAND_TYPE, ATTR_ARGS, IN, OUT, + CLIENT_COMMANDS, SERVER_COMMANDS, SERVER) + +_LOGGER = logging.getLogger(__name__) + +TYPE = 'type' +CLIENT = 'client' +ID = 'id' +NAME = 'name' +RESPONSE = 'response' +DEVICE_INFO = 'device_info' + +ATTR_DURATION = 'duration' +ATTR_IEEE_ADDRESS = 'ieee_address' +ATTR_IEEE = 'ieee' + +SERVICE_PERMIT = 'permit' +SERVICE_REMOVE = 'remove' +SERVICE_SET_ZIGBEE_CLUSTER_ATTRIBUTE = 'set_zigbee_cluster_attribute' +SERVICE_ISSUE_ZIGBEE_CLUSTER_COMMAND = 'issue_zigbee_cluster_command' +ZIGBEE_CLUSTER_SERVICE = 'zigbee_cluster_service' +IEEE_SERVICE = 'ieee_based_service' + +SERVICE_SCHEMAS = { + SERVICE_PERMIT: vol.Schema({ + vol.Optional(ATTR_DURATION, default=60): + vol.All(vol.Coerce(int), vol.Range(1, 254)), + }), + IEEE_SERVICE: vol.Schema({ + vol.Required(ATTR_IEEE_ADDRESS): cv.string, + }), + ZIGBEE_CLUSTER_SERVICE: vol.Schema({ + vol.Required(ATTR_ENTITY_ID): cv.entity_id, + vol.Required(ATTR_CLUSTER_ID): cv.positive_int, + vol.Optional(ATTR_CLUSTER_TYPE, default=IN): cv.string + }), + SERVICE_SET_ZIGBEE_CLUSTER_ATTRIBUTE: vol.Schema({ + vol.Required(ATTR_ENTITY_ID): cv.entity_id, + vol.Required(ATTR_CLUSTER_ID): cv.positive_int, + vol.Optional(ATTR_CLUSTER_TYPE, default=IN): cv.string, + vol.Required(ATTR_ATTRIBUTE): cv.positive_int, + vol.Required(ATTR_VALUE): cv.string, + vol.Optional(ATTR_MANUFACTURER): cv.positive_int, + }), + SERVICE_ISSUE_ZIGBEE_CLUSTER_COMMAND: vol.Schema({ + vol.Required(ATTR_ENTITY_ID): cv.entity_id, + vol.Required(ATTR_CLUSTER_ID): cv.positive_int, + vol.Optional(ATTR_CLUSTER_TYPE, default=IN): cv.string, + vol.Required(ATTR_COMMAND): cv.positive_int, + vol.Required(ATTR_COMMAND_TYPE): cv.string, + vol.Optional(ATTR_ARGS, default=''): cv.string, + vol.Optional(ATTR_MANUFACTURER): cv.positive_int, + }), +} + +WS_RECONFIGURE_NODE = 'zha/nodes/reconfigure' +SCHEMA_WS_RECONFIGURE_NODE = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ + vol.Required(TYPE): WS_RECONFIGURE_NODE, + vol.Required(ATTR_IEEE): str +}) + +WS_ENTITIES_BY_IEEE = 'zha/entities' +SCHEMA_WS_LIST = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ + vol.Required(TYPE): WS_ENTITIES_BY_IEEE, +}) + +WS_ENTITY_CLUSTERS = 'zha/entities/clusters' +SCHEMA_WS_CLUSTERS = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ + vol.Required(TYPE): WS_ENTITY_CLUSTERS, + vol.Required(ATTR_ENTITY_ID): cv.entity_id, + vol.Required(ATTR_IEEE): str +}) + +WS_ENTITY_CLUSTER_ATTRIBUTES = 'zha/entities/clusters/attributes' +SCHEMA_WS_CLUSTER_ATTRIBUTES = \ + websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ + vol.Required(TYPE): WS_ENTITY_CLUSTER_ATTRIBUTES, + vol.Required(ATTR_ENTITY_ID): cv.entity_id, + vol.Required(ATTR_IEEE): str, + vol.Required(ATTR_CLUSTER_ID): int, + vol.Required(ATTR_CLUSTER_TYPE): str + }) + +WS_READ_CLUSTER_ATTRIBUTE = 'zha/entities/clusters/attributes/value' +SCHEMA_WS_READ_CLUSTER_ATTRIBUTE = \ + websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ + vol.Required(TYPE): WS_READ_CLUSTER_ATTRIBUTE, + vol.Required(ATTR_ENTITY_ID): cv.entity_id, + vol.Required(ATTR_CLUSTER_ID): int, + vol.Required(ATTR_CLUSTER_TYPE): str, + vol.Required(ATTR_ATTRIBUTE): int, + vol.Optional(ATTR_MANUFACTURER): object, + }) + +WS_ENTITY_CLUSTER_COMMANDS = 'zha/entities/clusters/commands' +SCHEMA_WS_CLUSTER_COMMANDS = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ + vol.Required(TYPE): WS_ENTITY_CLUSTER_COMMANDS, + vol.Required(ATTR_ENTITY_ID): cv.entity_id, + vol.Required(ATTR_IEEE): str, + vol.Required(ATTR_CLUSTER_ID): int, + vol.Required(ATTR_CLUSTER_TYPE): str +}) + + +@websocket_api.async_response +async def websocket_entity_cluster_attributes(hass, connection, msg): + """Return a list of cluster attributes.""" + entity_id = msg[ATTR_ENTITY_ID] + cluster_id = msg[ATTR_CLUSTER_ID] + cluster_type = msg[ATTR_CLUSTER_TYPE] + component = hass.data.get(entity_id.split('.')[0]) + entity = component.get_entity(entity_id) + cluster_attributes = [] + if entity is not None: + res = await entity.get_cluster_attributes(cluster_id, cluster_type) + if res is not None: + for attr_id in res: + cluster_attributes.append( + { + ID: attr_id, + NAME: res[attr_id][0] + } + ) + _LOGGER.debug("Requested attributes for: %s %s %s %s", + "{}: [{}]".format(ATTR_CLUSTER_ID, cluster_id), + "{}: [{}]".format(ATTR_CLUSTER_TYPE, cluster_type), + "{}: [{}]".format(ATTR_ENTITY_ID, entity_id), + "{}: [{}]".format(RESPONSE, cluster_attributes) + ) + + connection.send_message(websocket_api.result_message( + msg[ID], + cluster_attributes + )) + + +@websocket_api.async_response +async def websocket_entity_cluster_commands(hass, connection, msg): + """Return a list of cluster commands.""" + entity_id = msg[ATTR_ENTITY_ID] + cluster_id = msg[ATTR_CLUSTER_ID] + cluster_type = msg[ATTR_CLUSTER_TYPE] + component = hass.data.get(entity_id.split('.')[0]) + entity = component.get_entity(entity_id) + cluster_commands = [] + if entity is not None: + res = await entity.get_cluster_commands(cluster_id, cluster_type) + if res is not None: + for cmd_id in res[CLIENT_COMMANDS]: + cluster_commands.append( + { + TYPE: CLIENT, + ID: cmd_id, + NAME: res[CLIENT_COMMANDS][cmd_id][0] + } + ) + for cmd_id in res[SERVER_COMMANDS]: + cluster_commands.append( + { + TYPE: SERVER, + ID: cmd_id, + NAME: res[SERVER_COMMANDS][cmd_id][0] + } + ) + _LOGGER.debug("Requested commands for: %s %s %s %s", + "{}: [{}]".format(ATTR_CLUSTER_ID, cluster_id), + "{}: [{}]".format(ATTR_CLUSTER_TYPE, cluster_type), + "{}: [{}]".format(ATTR_ENTITY_ID, entity_id), + "{}: [{}]".format(RESPONSE, cluster_commands) + ) + + connection.send_message(websocket_api.result_message( + msg[ID], + cluster_commands + )) + + +@websocket_api.async_response +async def websocket_read_zigbee_cluster_attributes(hass, connection, msg): + """Read zigbee attribute for cluster on zha entity.""" + entity_id = msg[ATTR_ENTITY_ID] + cluster_id = msg[ATTR_CLUSTER_ID] + cluster_type = msg[ATTR_CLUSTER_TYPE] + attribute = msg[ATTR_ATTRIBUTE] + component = hass.data.get(entity_id.split('.')[0]) + entity = component.get_entity(entity_id) + clusters = await entity.get_clusters() + cluster = clusters[cluster_type][cluster_id] + manufacturer = msg.get(ATTR_MANUFACTURER) or None + success = failure = None + if entity is not None: + success, failure = await cluster.read_attributes( + [attribute], + allow_cache=False, + only_cache=False, + manufacturer=manufacturer + ) + _LOGGER.debug("Read attribute for: %s %s %s %s %s %s %s", + "{}: [{}]".format(ATTR_CLUSTER_ID, cluster_id), + "{}: [{}]".format(ATTR_CLUSTER_TYPE, cluster_type), + "{}: [{}]".format(ATTR_ENTITY_ID, entity_id), + "{}: [{}]".format(ATTR_ATTRIBUTE, attribute), + "{}: [{}]".format(ATTR_MANUFACTURER, manufacturer), + "{}: [{}]".format(RESPONSE, str(success.get(attribute))), + "{}: [{}]".format('failure', failure) + ) + connection.send_message(websocket_api.result_message( + msg[ID], + str(success.get(attribute)) + )) + + +def async_load_api(hass, application_controller, listener): + """Set up the web socket API.""" + async def permit(service): + """Allow devices to join this network.""" + duration = service.data.get(ATTR_DURATION) + _LOGGER.info("Permitting joins for %ss", duration) + await application_controller.permit(duration) + + hass.services.async_register(DOMAIN, SERVICE_PERMIT, permit, + schema=SERVICE_SCHEMAS[SERVICE_PERMIT]) + + async def remove(service): + """Remove a node from the network.""" + from bellows.types import EmberEUI64, uint8_t + ieee = service.data.get(ATTR_IEEE_ADDRESS) + ieee = EmberEUI64([uint8_t(p, base=16) for p in ieee.split(':')]) + _LOGGER.info("Removing node %s", ieee) + await application_controller.remove(ieee) + + hass.services.async_register(DOMAIN, SERVICE_REMOVE, remove, + schema=SERVICE_SCHEMAS[IEEE_SERVICE]) + + async def set_zigbee_cluster_attributes(service): + """Set zigbee attribute for cluster on zha entity.""" + entity_id = service.data.get(ATTR_ENTITY_ID) + cluster_id = service.data.get(ATTR_CLUSTER_ID) + cluster_type = service.data.get(ATTR_CLUSTER_TYPE) + attribute = service.data.get(ATTR_ATTRIBUTE) + value = service.data.get(ATTR_VALUE) + manufacturer = service.data.get(ATTR_MANUFACTURER) or None + component = hass.data.get(entity_id.split('.')[0]) + entity = component.get_entity(entity_id) + response = None + if entity is not None: + response = await entity.write_zigbe_attribute( + cluster_id, + attribute, + value, + cluster_type=cluster_type, + manufacturer=manufacturer + ) + _LOGGER.debug("Set attribute for: %s %s %s %s %s %s %s", + "{}: [{}]".format(ATTR_CLUSTER_ID, cluster_id), + "{}: [{}]".format(ATTR_CLUSTER_TYPE, cluster_type), + "{}: [{}]".format(ATTR_ENTITY_ID, entity_id), + "{}: [{}]".format(ATTR_ATTRIBUTE, attribute), + "{}: [{}]".format(ATTR_VALUE, value), + "{}: [{}]".format(ATTR_MANUFACTURER, manufacturer), + "{}: [{}]".format(RESPONSE, response) + ) + + hass.services.async_register(DOMAIN, SERVICE_SET_ZIGBEE_CLUSTER_ATTRIBUTE, + set_zigbee_cluster_attributes, + schema=SERVICE_SCHEMAS[ + SERVICE_SET_ZIGBEE_CLUSTER_ATTRIBUTE + ]) + + async def issue_zigbee_cluster_command(service): + """Issue command on zigbee cluster on zha entity.""" + entity_id = service.data.get(ATTR_ENTITY_ID) + cluster_id = service.data.get(ATTR_CLUSTER_ID) + cluster_type = service.data.get(ATTR_CLUSTER_TYPE) + command = service.data.get(ATTR_COMMAND) + command_type = service.data.get(ATTR_COMMAND_TYPE) + args = service.data.get(ATTR_ARGS) + manufacturer = service.data.get(ATTR_MANUFACTURER) or None + component = hass.data.get(entity_id.split('.')[0]) + entity = component.get_entity(entity_id) + response = None + if entity is not None: + response = await entity.issue_cluster_command( + cluster_id, + command, + command_type, + args, + cluster_type=cluster_type, + manufacturer=manufacturer + ) + _LOGGER.debug("Issue command for: %s %s %s %s %s %s %s %s", + "{}: [{}]".format(ATTR_CLUSTER_ID, cluster_id), + "{}: [{}]".format(ATTR_CLUSTER_TYPE, cluster_type), + "{}: [{}]".format(ATTR_ENTITY_ID, entity_id), + "{}: [{}]".format(ATTR_COMMAND, command), + "{}: [{}]".format(ATTR_COMMAND_TYPE, command_type), + "{}: [{}]".format(ATTR_ARGS, args), + "{}: [{}]".format(ATTR_MANUFACTURER, manufacturer), + "{}: [{}]".format(RESPONSE, response) + ) + + hass.services.async_register(DOMAIN, SERVICE_ISSUE_ZIGBEE_CLUSTER_COMMAND, + issue_zigbee_cluster_command, + schema=SERVICE_SCHEMAS[ + SERVICE_ISSUE_ZIGBEE_CLUSTER_COMMAND + ]) + + @websocket_api.async_response + async def websocket_reconfigure_node(hass, connection, msg): + """Reconfigure a ZHA nodes entities by its ieee address.""" + ieee = msg[ATTR_IEEE] + entities = listener.get_entities_for_ieee(ieee) + _LOGGER.debug("Reconfiguring node with ieee_address: %s", ieee) + for entity in entities: + if hasattr(entity, 'async_configure'): + hass.async_create_task(entity.async_configure()) + + hass.components.websocket_api.async_register_command( + WS_RECONFIGURE_NODE, websocket_reconfigure_node, + SCHEMA_WS_RECONFIGURE_NODE + ) + + @websocket_api.async_response + async def websocket_entities_by_ieee(hass, connection, msg): + """Return a dict of all zha entities grouped by ieee.""" + entities_by_ieee = {} + for ieee, entities in listener.device_registry.items(): + ieee_string = str(ieee) + entities_by_ieee[ieee_string] = [] + for entity in entities: + if not isinstance(entity, ZhaDeviceEntity): + entities_by_ieee[ieee_string].append({ + ATTR_ENTITY_ID: entity.entity_id, + DEVICE_INFO: entity.device_info + }) + connection.send_message(websocket_api.result_message( + msg[ID], + entities_by_ieee + )) + + hass.components.websocket_api.async_register_command( + WS_ENTITIES_BY_IEEE, websocket_entities_by_ieee, + SCHEMA_WS_LIST + ) + + @websocket_api.async_response + async def websocket_entity_clusters(hass, connection, msg): + """Return a list of entity clusters.""" + entity_id = msg[ATTR_ENTITY_ID] + entities = listener.get_entities_for_ieee(msg[ATTR_IEEE]) + entity = next( + ent for ent in entities if ent.entity_id == entity_id) + entity_clusters = await entity.get_clusters() + clusters = [] + + for cluster_id, cluster in entity_clusters[IN].items(): + clusters.append({ + TYPE: IN, + ID: cluster_id, + NAME: cluster.__class__.__name__ + }) + for cluster_id, cluster in entity_clusters[OUT].items(): + clusters.append({ + TYPE: OUT, + ID: cluster_id, + NAME: cluster.__class__.__name__ + }) + + connection.send_message(websocket_api.result_message( + msg[ID], + clusters + )) + + hass.components.websocket_api.async_register_command( + WS_ENTITY_CLUSTERS, websocket_entity_clusters, + SCHEMA_WS_CLUSTERS + ) + + hass.components.websocket_api.async_register_command( + WS_ENTITY_CLUSTER_ATTRIBUTES, websocket_entity_cluster_attributes, + SCHEMA_WS_CLUSTER_ATTRIBUTES + ) + + hass.components.websocket_api.async_register_command( + WS_ENTITY_CLUSTER_COMMANDS, websocket_entity_cluster_commands, + SCHEMA_WS_CLUSTER_COMMANDS + ) + + hass.components.websocket_api.async_register_command( + WS_READ_CLUSTER_ATTRIBUTE, websocket_read_zigbee_cluster_attributes, + SCHEMA_WS_READ_CLUSTER_ATTRIBUTE + ) + + +def async_unload_api(hass): + """Unload the ZHA API.""" + hass.services.async_remove(DOMAIN, SERVICE_PERMIT) + hass.services.async_remove(DOMAIN, SERVICE_REMOVE) + hass.services.async_remove(DOMAIN, SERVICE_SET_ZIGBEE_CLUSTER_ATTRIBUTE) + hass.services.async_remove(DOMAIN, SERVICE_ISSUE_ZIGBEE_CLUSTER_COMMAND) diff --git a/homeassistant/components/zha/const.py b/homeassistant/components/zha/const.py index 5b650c95cc4..29295e35060 100644 --- a/homeassistant/components/zha/const.py +++ b/homeassistant/components/zha/const.py @@ -36,6 +36,21 @@ DEFAULT_RADIO_TYPE = 'ezsp' DEFAULT_BAUDRATE = 57600 DEFAULT_DATABASE_NAME = 'zigbee.db' +ATTR_CLUSTER_ID = 'cluster_id' +ATTR_CLUSTER_TYPE = 'cluster_type' +ATTR_ATTRIBUTE = 'attribute' +ATTR_VALUE = 'value' +ATTR_MANUFACTURER = 'manufacturer' +ATTR_COMMAND = 'command' +ATTR_COMMAND_TYPE = 'command_type' +ATTR_ARGS = 'args' + +IN = 'in' +OUT = 'out' +CLIENT_COMMANDS = 'client_commands' +SERVER_COMMANDS = 'server_commands' +SERVER = 'server' + class RadioType(enum.Enum): """Possible options for radio type.""" diff --git a/homeassistant/components/zha/entities/entity.py b/homeassistant/components/zha/entities/entity.py index dadd79e82a5..2b8ee2f1748 100644 --- a/homeassistant/components/zha/entities/entity.py +++ b/homeassistant/components/zha/entities/entity.py @@ -9,8 +9,11 @@ import logging from random import uniform from homeassistant.components.zha.const import ( - DATA_ZHA, DATA_ZHA_BRIDGE_ID, DOMAIN) + DATA_ZHA, DATA_ZHA_BRIDGE_ID, DOMAIN, ATTR_CLUSTER_ID, ATTR_ATTRIBUTE, + ATTR_VALUE, ATTR_MANUFACTURER, ATTR_COMMAND, SERVER, ATTR_COMMAND_TYPE, + ATTR_ARGS, IN, OUT, CLIENT_COMMANDS, SERVER_COMMANDS) from homeassistant.components.zha.helpers import bind_configure_reporting +from homeassistant.const import ATTR_ENTITY_ID, CONF_FRIENDLY_NAME from homeassistant.core import callback from homeassistant.helpers import entity from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE @@ -18,6 +21,8 @@ from homeassistant.util import slugify _LOGGER = logging.getLogger(__name__) +ENTITY_SUFFIX = 'entity_suffix' + class ZhaEntity(entity.Entity): """A base class for ZHA entities.""" @@ -38,9 +43,9 @@ class ZhaEntity(entity.Entity): slugify(model), ieeetail, endpoint.endpoint_id, - kwargs.get('entity_suffix', ''), + kwargs.get(ENTITY_SUFFIX, ''), ) - self._device_state_attributes['friendly_name'] = "{} {}".format( + self._device_state_attributes[CONF_FRIENDLY_NAME] = "{} {}".format( manufacturer, model, ) @@ -49,7 +54,7 @@ class ZhaEntity(entity.Entity): self._domain, ieeetail, endpoint.endpoint_id, - kwargs.get('entity_suffix', ''), + kwargs.get(ENTITY_SUFFIX, ''), ) self._endpoint = endpoint @@ -69,6 +74,100 @@ class ZhaEntity(entity.Entity): self.manufacturer_code = None application_listener.register_entity(ieee, self) + async def get_clusters(self): + """Get zigbee clusters from this entity.""" + return { + IN: self._in_clusters, + OUT: self._out_clusters + } + + async def _get_cluster(self, cluster_id, cluster_type=IN): + """Get zigbee cluster from this entity.""" + if cluster_type == IN: + cluster = self._in_clusters[cluster_id] + else: + cluster = self._out_clusters[cluster_id] + if cluster is None: + _LOGGER.warning('in_cluster with id: %s not found on entity: %s', + cluster_id, self.entity_id) + return cluster + + async def get_cluster_attributes(self, cluster_id, cluster_type=IN): + """Get zigbee attributes for specified cluster.""" + cluster = await self._get_cluster(cluster_id, cluster_type) + if cluster is None: + return + return cluster.attributes + + async def write_zigbe_attribute(self, cluster_id, attribute, value, + cluster_type=IN, manufacturer=None): + """Write a value to a zigbee attribute for a cluster in this entity.""" + cluster = await self._get_cluster(cluster_id, cluster_type) + if cluster is None: + return + + from zigpy.exceptions import DeliveryError + try: + response = await cluster.write_attributes( + {attribute: value}, + manufacturer=manufacturer + ) + _LOGGER.debug( + 'set: %s for attr: %s to cluster: %s for entity: %s - res: %s', + value, + attribute, + cluster_id, + self.entity_id, + response + ) + return response + except DeliveryError as exc: + _LOGGER.debug( + 'failed to set attribute: %s %s %s %s %s', + '{}: {}'.format(ATTR_VALUE, value), + '{}: {}'.format(ATTR_ATTRIBUTE, attribute), + '{}: {}'.format(ATTR_CLUSTER_ID, cluster_id), + '{}: {}'.format(ATTR_ENTITY_ID, self.entity_id), + exc + ) + + async def get_cluster_commands(self, cluster_id, cluster_type=IN): + """Get zigbee commands for specified cluster.""" + cluster = await self._get_cluster(cluster_id, cluster_type) + if cluster is None: + return + return { + CLIENT_COMMANDS: cluster.client_commands, + SERVER_COMMANDS: cluster.server_commands, + } + + async def issue_cluster_command(self, cluster_id, command, command_type, + args, cluster_type=IN, + manufacturer=None): + """Issue a command against specified zigbee cluster on this entity.""" + cluster = await self._get_cluster(cluster_id, cluster_type) + if cluster is None: + return + response = None + if command_type == SERVER: + response = await cluster.command(command, *args, + manufacturer=manufacturer, + expect_reply=True) + else: + response = await cluster.client_command(command, *args) + + _LOGGER.debug( + 'Issued cluster command: %s %s %s %s %s %s %s', + '{}: {}'.format(ATTR_CLUSTER_ID, cluster_id), + '{}: {}'.format(ATTR_COMMAND, command), + '{}: {}'.format(ATTR_COMMAND_TYPE, command_type), + '{}: {}'.format(ATTR_ARGS, args), + '{}: {}'.format(ATTR_CLUSTER_ID, cluster_type), + '{}: {}'.format(ATTR_MANUFACTURER, manufacturer), + '{}: {}'.format(ATTR_ENTITY_ID, self.entity_id) + ) + return response + async def async_added_to_hass(self): """Handle entity addition to hass. @@ -201,9 +300,12 @@ class ZhaEntity(entity.Entity): return { 'connections': {(CONNECTION_ZIGBEE, ieee)}, 'identifiers': {(DOMAIN, ieee)}, - 'manufacturer': self._endpoint.manufacturer, + ATTR_MANUFACTURER: self._endpoint.manufacturer, 'model': self._endpoint.model, - 'name': self._device_state_attributes.get('friendly_name', ieee), + 'name': self._device_state_attributes.get( + CONF_FRIENDLY_NAME, + ieee + ), 'via_hub': (DOMAIN, self.hass.data[DATA_ZHA][DATA_ZHA_BRIDGE_ID]), } diff --git a/homeassistant/components/zha/helpers.py b/homeassistant/components/zha/helpers.py index 3212849f721..b6c09dd1fce 100644 --- a/homeassistant/components/zha/helpers.py +++ b/homeassistant/components/zha/helpers.py @@ -14,7 +14,8 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -async def safe_read(cluster, attributes, allow_cache=True, only_cache=False): +async def safe_read(cluster, attributes, allow_cache=True, only_cache=False, + manufacturer=None): """Swallow all exceptions from network read. If we throw during initialization, setup fails. Rather have an entity that @@ -25,7 +26,8 @@ async def safe_read(cluster, attributes, allow_cache=True, only_cache=False): result, _ = await cluster.read_attributes( attributes, allow_cache=allow_cache, - only_cache=only_cache + only_cache=only_cache, + manufacturer=manufacturer ) return result except Exception: # pylint: disable=broad-except @@ -124,3 +126,9 @@ async def check_zigpy_connection(usb_path, radio_type, database_path): except Exception: # pylint: disable=broad-except return False return True + + +def convert_ieee(ieee_str): + """Convert given ieee string to EUI64.""" + from zigpy.types import EUI64, uint8_t + return EUI64([uint8_t(p, base=16) for p in ieee_str.split(':')]) diff --git a/homeassistant/components/zha/services.yaml b/homeassistant/components/zha/services.yaml index 4b1122b8167..c328d69a6c3 100644 --- a/homeassistant/components/zha/services.yaml +++ b/homeassistant/components/zha/services.yaml @@ -13,3 +13,63 @@ remove: ieee_address: description: IEEE address of the node to remove example: "00:0d:6f:00:05:7d:2d:34" + +reconfigure_device: + description: >- + Reconfigure ZHA device (heal device). Use this if you are having issues + with the device. If the device in question is a battery powered device + please ensure it is awake and accepting commands when you use this + service. + fields: + ieee_address: + description: IEEE address of the device to reconfigure + example: "00:0d:6f:00:05:7d:2d:34" + +set_zigbee_cluster_attribute: + description: >- + Set attribute value for the specified cluster on the specified entity. + fields: + entity_id: + description: Entity id + example: "binary_sensor.centralite_3130_00e8fb4e_1" + cluster_id: + description: ZCL cluster to retrieve attributes for + example: 6 + cluster_type: + description: type of the cluster (in or out) + example: "out" + attribute: + description: id of the attribute to set + example: 0 + value: + description: value to write to the attribute + example: 0x0001 + manufacturer: + description: manufacturer code + example: 0x00FC + +issue_zigbee_cluster_command: + description: >- + Issue command on the specified cluster on the specified entity. + fields: + entity_id: + description: Entity id + example: "binary_sensor.centralite_3130_00e8fb4e_1" + cluster_id: + description: ZCL cluster to retrieve attributes for + example: 6 + cluster_type: + description: type of the cluster (in or out) + example: "out" + command: + description: id of the command to execute + example: 0 + command_type: + description: type of the command to execute (client or server) + example: "server" + args: + description: args to pass to the command + example: {} + manufacturer: + description: manufacturer code + example: 0x00FC From 7dbbea22386d6f065c49d8ea8784f3ca5f2b11d5 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 11 Jan 2019 11:48:32 -0800 Subject: [PATCH 105/235] Updated frontend to 20190109.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 478ff5549d9..026311f1397 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -24,7 +24,7 @@ from homeassistant.core import callback from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass -REQUIREMENTS = ['home-assistant-frontend==20190109.0'] +REQUIREMENTS = ['home-assistant-frontend==20190109.1'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log', diff --git a/requirements_all.txt b/requirements_all.txt index 775bf5b6928..2adbfcbdde2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -514,7 +514,7 @@ hole==0.3.0 holidays==0.9.9 # homeassistant.components.frontend -home-assistant-frontend==20190109.0 +home-assistant-frontend==20190109.1 # homeassistant.components.zwave homeassistant-pyozw==0.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3e60bb511ad..d1ca93ae80f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -107,7 +107,7 @@ hdate==0.8.7 holidays==0.9.9 # homeassistant.components.frontend -home-assistant-frontend==20190109.0 +home-assistant-frontend==20190109.1 # homeassistant.components.homematicip_cloud homematicip==0.10.3 From 99c2e4ac44e6decd263b752e5adb835f17180f18 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 11 Jan 2019 11:52:07 -0800 Subject: [PATCH 106/235] Fix warning (#19946) * Fix warning * Update service.py --- homeassistant/helpers/service.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index 66488fbec3d..2b7638b55ee 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -204,8 +204,9 @@ async def entity_service_call(hass, platforms, func, call): if ATTR_ENTITY_ID in call.data: target_all_entities = call.data[ATTR_ENTITY_ID] == ENTITY_MATCH_ALL else: - _LOGGER.warning('Not passing an entity ID to a service to target all ' - 'entities is deprecated. Use instead: entity_id: "*"') + _LOGGER.warning( + 'Not passing an entity ID to a service to target all entities is ' + 'deprecated. Use instead: entity_id: "%s"', ENTITY_MATCH_ALL) target_all_entities = True if not target_all_entities: From 8ef2f1f67bc8c60bfb8a22ea1a88f3c2718eadbc Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 11 Jan 2019 11:58:14 -0800 Subject: [PATCH 107/235] Fix warning (#19946) * Fix warning * Update service.py From 14dd8791ec6984a608e83877f227d93dcd6031c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antoine=20GR=C3=89A?= Date: Fri, 11 Jan 2019 21:54:47 +0100 Subject: [PATCH 108/235] Adding IPv6 to fail2ban sensor (#19457) * Fixing fail2ban regex for ipv6 * Adding IPv6 tests for fail2ban * Formating code for hound * Formating again * Formating again 2 --- homeassistant/components/sensor/fail2ban.py | 2 +- tests/components/sensor/test_fail2ban.py | 21 +++++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/fail2ban.py b/homeassistant/components/sensor/fail2ban.py index 6080ce9fd10..78b11b1942b 100644 --- a/homeassistant/components/sensor/fail2ban.py +++ b/homeassistant/components/sensor/fail2ban.py @@ -64,7 +64,7 @@ class BanSensor(Entity): self.last_ban = None self.log_parser = log_parser self.log_parser.ip_regex[self.jail] = re.compile( - r"\[{}\].(Ban|Unban) ([\w+\.]{{3,}})".format(re.escape(self.jail)) + r"\[{}\]\s*(Ban|Unban) (.*)".format(re.escape(self.jail)) ) _LOGGER.debug("Setting up jail %s", self.jail) diff --git a/tests/components/sensor/test_fail2ban.py b/tests/components/sensor/test_fail2ban.py index a48b7027725..e7874765950 100644 --- a/tests/components/sensor/test_fail2ban.py +++ b/tests/components/sensor/test_fail2ban.py @@ -19,6 +19,10 @@ def fake_log(log_key): '2017-01-01 12:23:35 fail2ban.actions [111]: ' 'NOTICE [jail_one] Ban 111.111.111.111' ), + 'ipv6_ban': ( + '2017-01-01 12:23:35 fail2ban.actions [111]: ' + 'NOTICE [jail_one] Ban 2607:f0d0:1002:51::4' + ), 'multi_ban': ( '2017-01-01 12:23:35 fail2ban.actions [111]: ' 'NOTICE [jail_one] Ban 111.111.111.111\n' @@ -112,6 +116,23 @@ class TestBanSensor(unittest.TestCase): assert \ sensor.state_attributes[STATE_ALL_BANS] == ['111.111.111.111'] + def test_ipv6_ban(self): + """Test that log is parsed correctly for IPV6 bans.""" + log_parser = BanLogParser(timedelta(seconds=-1), '/tmp') + sensor = BanSensor('fail2ban', 'jail_one', log_parser) + assert sensor.name == 'fail2ban jail_one' + mock_fh = MockOpen(read_data=fake_log('ipv6_ban')) + with patch('homeassistant.components.sensor.fail2ban.open', mock_fh, + create=True): + sensor.update() + + assert sensor.state == '2607:f0d0:1002:51::4' + assert \ + sensor.state_attributes[STATE_CURRENT_BANS] == \ + ['2607:f0d0:1002:51::4'] + assert \ + sensor.state_attributes[STATE_ALL_BANS] == ['2607:f0d0:1002:51::4'] + def test_multiple_ban(self): """Test that log is parsed correctly for multiple ban.""" log_parser = BanLogParser('/tmp') From 12d16d9bdccf1b75f17d5f7314bc334ed61ee73a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 11 Jan 2019 12:55:23 -0800 Subject: [PATCH 109/235] Update test_auth.py --- tests/components/hassio/test_auth.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/components/hassio/test_auth.py b/tests/components/hassio/test_auth.py index 44eedde5d1d..ed34ea96b49 100644 --- a/tests/components/hassio/test_auth.py +++ b/tests/components/hassio/test_auth.py @@ -4,7 +4,7 @@ from unittest.mock import patch, Mock from homeassistant.const import HTTP_HEADER_HA_AUTH from homeassistant.exceptions import HomeAssistantError -from tests.common import mock_coro, register_auth_provider +from tests.common import mock_coro from . import API_PASSWORD From a65d14c0cd70bd0fbf4d3a08fa923dbd41a78766 Mon Sep 17 00:00:00 2001 From: Mike Miller Date: Fri, 11 Jan 2019 22:58:14 +0200 Subject: [PATCH 110/235] Always use datetime and timedelta in camera.proxy instead of int/float (#19571) --- homeassistant/components/camera/proxy.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/camera/proxy.py b/homeassistant/components/camera/proxy.py index 943e4986623..8afd71abc26 100644 --- a/homeassistant/components/camera/proxy.py +++ b/homeassistant/components/camera/proxy.py @@ -7,6 +7,7 @@ https://www.home-assistant.io/components/camera.proxy/ import asyncio import logging +from datetime import timedelta import voluptuous as vol from homeassistant.components.camera import PLATFORM_SCHEMA, Camera @@ -206,7 +207,7 @@ class ProxyCamera(Camera): self._cache_images = bool( config.get(CONF_IMAGE_REFRESH_RATE) or config.get(CONF_CACHE_IMAGES)) - self._last_image_time = 0 + self._last_image_time = dt_util.utc_from_timestamp(0) self._last_image = None self._headers = ( {HTTP_HEADER_HA_AUTH: self.hass.config.api.api_password} @@ -223,7 +224,8 @@ class ProxyCamera(Camera): now = dt_util.utcnow() if (self._image_refresh_rate and - now < self._last_image_time + self._image_refresh_rate): + now < self._last_image_time + + timedelta(seconds=self._image_refresh_rate)): return self._last_image self._last_image_time = now From 25408bd48398fab1ac10516edbce3eed1280129f Mon Sep 17 00:00:00 2001 From: Oliver Date: Fri, 11 Jan 2019 22:04:56 +0100 Subject: [PATCH 111/235] Include Scripts/ directory to .gitignore - this is created by virtualenv on Windows (#18918) --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index c2b0d964a62..91b8d024aed 100644 --- a/.gitignore +++ b/.gitignore @@ -78,6 +78,7 @@ venv .venv Pipfile* share/* +Scripts/ # vimmy stuff *.swp From 937688f7a6caa9d7efc4dd72932a10156ede26e3 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Fri, 11 Jan 2019 22:06:06 +0100 Subject: [PATCH 112/235] Add mysensors state update delay (#18891) * Add mysensors state update delay * Schedule state update after delay to avoid updating state multiple times during the same short timespan. * Code review --- .../components/device_tracker/mysensors.py | 7 ++-- homeassistant/components/mysensors/const.py | 1 + homeassistant/components/mysensors/device.py | 33 ++++++++++++++++--- homeassistant/components/mysensors/handler.py | 1 - 4 files changed, 34 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/device_tracker/mysensors.py b/homeassistant/components/device_tracker/mysensors.py index 8b10bc2b9bb..705dc9968c9 100644 --- a/homeassistant/components/device_tracker/mysensors.py +++ b/homeassistant/components/device_tracker/mysensors.py @@ -14,7 +14,7 @@ async def async_setup_scanner(hass, config, async_see, discovery_info=None): """Set up the MySensors device scanner.""" new_devices = mysensors.setup_mysensors_platform( hass, DOMAIN, discovery_info, MySensorsDeviceScanner, - device_args=(async_see, )) + device_args=(hass, async_see)) if not new_devices: return False @@ -37,12 +37,13 @@ async def async_setup_scanner(hass, config, async_see, discovery_info=None): class MySensorsDeviceScanner(mysensors.device.MySensorsDevice): """Represent a MySensors scanner.""" - def __init__(self, async_see, *args): + def __init__(self, hass, async_see, *args): """Set up instance.""" super().__init__(*args) self.async_see = async_see + self.hass = hass - async def async_update_callback(self): + async def _async_update_callback(self): """Update the device.""" await self.async_update() node = self.gateway.sensors[self.node_id] diff --git a/homeassistant/components/mysensors/const.py b/homeassistant/components/mysensors/const.py index ccb54bf647f..fbe3621a4af 100644 --- a/homeassistant/components/mysensors/const.py +++ b/homeassistant/components/mysensors/const.py @@ -23,6 +23,7 @@ SCHEMA = 'schema' CHILD_CALLBACK = 'mysensors_child_callback_{}_{}_{}_{}' NODE_CALLBACK = 'mysensors_node_callback_{}_{}' TYPE = 'type' +UPDATE_DELAY = 0.1 # MySensors const schemas BINARY_SENSOR_SCHEMA = {PLATFORM: 'binary_sensor', TYPE: 'V_TRIPPED'} diff --git a/homeassistant/components/mysensors/device.py b/homeassistant/components/mysensors/device.py index 07261b1c2a6..54333d8699b 100644 --- a/homeassistant/components/mysensors/device.py +++ b/homeassistant/components/mysensors/device.py @@ -1,5 +1,6 @@ """Handle MySensors devices.""" import logging +from functools import partial from homeassistant.const import ( ATTR_BATTERY_LEVEL, STATE_OFF, STATE_ON) @@ -7,7 +8,7 @@ from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity -from .const import CHILD_CALLBACK, NODE_CALLBACK +from .const import CHILD_CALLBACK, NODE_CALLBACK, UPDATE_DELAY _LOGGER = logging.getLogger(__name__) @@ -39,6 +40,8 @@ class MySensorsDevice: child = gateway.sensors[node_id].children[child_id] self.child_type = child.type self._values = {} + self._update_scheduled = False + self.hass = None @property def name(self): @@ -84,6 +87,29 @@ class MySensorsDevice: else: self._values[value_type] = value + async def _async_update_callback(self): + """Update the device.""" + raise NotImplementedError + + @callback + def async_update_callback(self): + """Update the device after delay.""" + if self._update_scheduled: + return + + async def update(): + """Perform update.""" + try: + await self._async_update_callback() + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Error updating %s", self.name) + finally: + self._update_scheduled = False + + self._update_scheduled = True + delayed_update = partial(self.hass.async_create_task, update()) + self.hass.loop.call_later(UPDATE_DELAY, delayed_update) + class MySensorsEntity(MySensorsDevice, Entity): """Representation of a MySensors entity.""" @@ -98,10 +124,9 @@ class MySensorsEntity(MySensorsDevice, Entity): """Return true if entity is available.""" return self.value_type in self._values - @callback - def async_update_callback(self): + async def _async_update_callback(self): """Update the entity.""" - self.async_schedule_update_ha_state(True) + await self.async_update_ha_state(True) async def async_added_to_hass(self): """Register update callback.""" diff --git a/homeassistant/components/mysensors/handler.py b/homeassistant/components/mysensors/handler.py index 39af1173706..886660baffe 100644 --- a/homeassistant/components/mysensors/handler.py +++ b/homeassistant/components/mysensors/handler.py @@ -99,7 +99,6 @@ def _handle_child_update(hass, hass_config, msg): for signal in set(signals): # 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. async_dispatcher_send(hass, signal) From 199db7219ec563e075c335dece86b0c3de3c9f3e Mon Sep 17 00:00:00 2001 From: Matt Snyder Date: Fri, 11 Jan 2019 15:07:12 -0600 Subject: [PATCH 113/235] Add ability to monitor relay events (#18730) * Add ability to monitor relay events * Account for empty events array instead of none. --- homeassistant/components/doorbird.py | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/doorbird.py b/homeassistant/components/doorbird.py index 42d14205e75..a3aab7e8dd9 100644 --- a/homeassistant/components/doorbird.py +++ b/homeassistant/components/doorbird.py @@ -25,6 +25,7 @@ API_URL = '/api/{}'.format(DOMAIN) CONF_CUSTOM_URL = 'hass_url_override' CONF_DOORBELL_EVENTS = 'doorbell_events' CONF_DOORBELL_NUMS = 'doorbell_numbers' +CONF_RELAY_NUMS = 'relay_numbers' CONF_MOTION_EVENTS = 'motion_events' CONF_TOKEN = 'token' @@ -37,6 +38,10 @@ SENSOR_TYPES = { 'name': 'Motion', 'device_class': 'motion', }, + 'relay': { + 'name': 'Relay', + 'device_class': 'relay', + } } RESET_DEVICE_FAVORITES = 'doorbird_reset_favorites' @@ -47,6 +52,8 @@ DEVICE_SCHEMA = vol.Schema({ vol.Required(CONF_PASSWORD): cv.string, vol.Optional(CONF_DOORBELL_NUMS, default=[1]): vol.All( cv.ensure_list, [cv.positive_int]), + vol.Optional(CONF_RELAY_NUMS, default=[1]): vol.All( + cv.ensure_list, [cv.positive_int]), vol.Optional(CONF_CUSTOM_URL): cv.string, vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_MONITORED_CONDITIONS, default=[]): @@ -80,6 +87,7 @@ def setup(hass, config): username = doorstation_config.get(CONF_USERNAME) password = doorstation_config.get(CONF_PASSWORD) doorbell_nums = doorstation_config.get(CONF_DOORBELL_NUMS) + relay_nums = doorstation_config.get(CONF_RELAY_NUMS) custom_url = doorstation_config.get(CONF_CUSTOM_URL) events = doorstation_config.get(CONF_MONITORED_CONDITIONS) name = (doorstation_config.get(CONF_NAME) @@ -90,7 +98,7 @@ def setup(hass, config): if status[0]: doorstation = ConfiguredDoorBird(device, name, events, custom_url, - doorbell_nums, token) + doorbell_nums, relay_nums, token) doorstations.append(doorstation) _LOGGER.info('Connected to DoorBird "%s" as %s@%s', doorstation.name, username, device_ip) @@ -148,13 +156,15 @@ def handle_event(event): class ConfiguredDoorBird(): """Attach additional information to pass along with configured device.""" - def __init__(self, device, name, events, custom_url, doorbell_nums, token): + def __init__(self, device, name, events, custom_url, doorbell_nums, + relay_nums, token): """Initialize configured device.""" self._name = name self._device = device self._custom_url = custom_url self._monitored_events = events self._doorbell_nums = doorbell_nums + self._relay_nums = relay_nums self._token = token @property @@ -218,9 +228,8 @@ class ConfiguredDoorBird(): # Register HA URL as webhook if not already, then get the ID if not self.webhook_is_registered(hass_url): - self.device.change_favorite('http', - 'Home Assistant on {} ({} events)' - .format(hass_url, event), hass_url) + self.device.change_favorite('http', 'Home Assistant ({} events)' + .format(event), hass_url) fav_id = self.get_webhook_id(hass_url) if not fav_id: @@ -239,6 +248,13 @@ class ConfiguredDoorBird(): entry = self.device.get_schedule_entry(event, str(doorbell)) entry.output.append(output) self.device.change_schedule(entry) + elif event == 'relay': + # Repeat edit for each monitored doorbell number + for relay in self._relay_nums: + entry = self.device.get_schedule_entry(event, str(relay)) + entry.output.append(output) + resp = self.device.change_schedule(entry) + return resp else: entry = self.device.get_schedule_entry(event) entry.output.append(output) From b9a488912ae75a8af64462992f2bfd775363d153 Mon Sep 17 00:00:00 2001 From: Fredrik Erlandsson Date: Fri, 11 Jan 2019 22:11:13 +0100 Subject: [PATCH 114/235] Add support for 'via_hub' for device_info (#19454) * Add support for 'via_hub' * Update config schema * add domain to via_hub * add tests for via_hub --- homeassistant/components/mqtt/__init__.py | 5 ++++ tests/components/mqtt/test_init.py | 13 +++++++++ tests/components/sensor/test_mqtt.py | 33 +++++++++++++++++++++++ 3 files changed, 51 insertions(+) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index c740086ba2f..801cb2997d4 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -80,6 +80,7 @@ CONF_CONNECTIONS = 'connections' CONF_MANUFACTURER = 'manufacturer' CONF_MODEL = 'model' CONF_SW_VERSION = 'sw_version' +CONF_VIA_HUB = 'via_hub' PROTOCOL_31 = '3.1' PROTOCOL_311 = '3.1.1' @@ -224,6 +225,7 @@ MQTT_ENTITY_DEVICE_INFO_SCHEMA = vol.All(vol.Schema({ vol.Optional(CONF_MODEL): cv.string, vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_SW_VERSION): cv.string, + vol.Optional(CONF_VIA_HUB): cv.string, }), validate_device_has_at_least_one_identifier) MQTT_JSON_ATTRS_SCHEMA = vol.Schema({ @@ -1032,4 +1034,7 @@ class MqttEntityDeviceInfo(Entity): if CONF_SW_VERSION in self._device_config: info['sw_version'] = self._device_config[CONF_SW_VERSION] + if CONF_VIA_HUB in self._device_config: + info['via_hub'] = (DOMAIN, self._device_config[CONF_VIA_HUB]) + return info diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index 81e6a7b298d..6652eddd20b 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -231,6 +231,19 @@ class TestMQTTComponent(unittest.TestCase): 'model': 'Glass', 'sw_version': '0.1-beta', }) + # full device info with via_hub + mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA({ + 'identifiers': ['helloworld', 'hello'], + 'connections': [ + ["mac", "02:5b:26:a8:dc:12"], + ["zigbee", "zigbee_id"], + ], + 'manufacturer': 'Whatever', + 'name': 'Beer', + 'model': 'Glass', + 'sw_version': '0.1-beta', + 'via_hub': 'test-hub', + }) # no identifiers with pytest.raises(vol.Invalid): mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA({ diff --git a/tests/components/sensor/test_mqtt.py b/tests/components/sensor/test_mqtt.py index 739e81258c2..f5eb8b23cf1 100644 --- a/tests/components/sensor/test_mqtt.py +++ b/tests/components/sensor/test_mqtt.py @@ -610,3 +610,36 @@ async def test_entity_id_update(hass, mqtt_mock): assert mock_mqtt.async_subscribe.call_count == 2 mock_mqtt.async_subscribe.assert_any_call('test-topic', ANY, 0, 'utf-8') mock_mqtt.async_subscribe.assert_any_call('avty-topic', ANY, 0, 'utf-8') + + +async def test_entity_device_info_with_hub(hass, mqtt_mock): + """Test MQTT sensor device registry integration.""" + entry = MockConfigEntry(domain=mqtt.DOMAIN) + entry.add_to_hass(hass) + await async_start(hass, 'homeassistant', {}, entry) + + registry = await hass.helpers.device_registry.async_get_registry() + hub = registry.async_get_or_create( + config_entry_id='123', + connections=set(), + identifiers={('mqtt', 'hub-id')}, + manufacturer='manufacturer', model='hub' + ) + + data = json.dumps({ + 'platform': 'mqtt', + 'name': 'Test 1', + 'state_topic': 'test-topic', + 'device': { + 'identifiers': ['helloworld'], + 'via_hub': 'hub-id', + }, + 'unique_id': 'veryunique' + }) + async_fire_mqtt_message(hass, 'homeassistant/sensor/bla/config', data) + await hass.async_block_till_done() + await hass.async_block_till_done() + + device = registry.async_get_device({('mqtt', 'helloworld')}, set()) + assert device is not None + assert device.hub_device_id == hub.id From ab4e1fddd5fe48aad2448dc6ad74bacbfefda4ac Mon Sep 17 00:00:00 2001 From: Andrew Chatham Date: Fri, 11 Jan 2019 14:34:32 -0800 Subject: [PATCH 115/235] Fix the anthemav component by removing a debugging line. (#19979) This lane ended up calling vars(transport) on an asyncio Transport object. In a standard setup, that's a python object provided by syncio, and it works. Home Assistant injects uvloop into asyncio, which makes this a Python C object, and those don't support vars(). --- homeassistant/components/media_player/anthemav.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/media_player/anthemav.py b/homeassistant/components/media_player/anthemav.py index f1954a1d37e..0a9f208dae4 100644 --- a/homeassistant/components/media_player/anthemav.py +++ b/homeassistant/components/media_player/anthemav.py @@ -59,7 +59,6 @@ async def async_setup_platform(hass, config, async_add_entities, _LOGGER.debug("dump_devicedata: %s", device.dump_avrdata) _LOGGER.debug("dump_conndata: %s", avr.dump_conndata) - _LOGGER.debug("dump_rawdata: %s", avr.protocol.dump_rawdata) hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, device.avr.close) async_add_entities([device]) From 8c27bf8c7c20973c105ff94c96b4a90c24991c41 Mon Sep 17 00:00:00 2001 From: Bas Date: Fri, 11 Jan 2019 23:59:31 +0100 Subject: [PATCH 116/235] Expose more information about shipments by PostNL (#18334) * Expose more information about shipments by PostNL * Update postnl.py --- homeassistant/components/sensor/postnl.py | 21 +++++---------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/sensor/postnl.py b/homeassistant/components/sensor/postnl.py index 5c70ca035cf..84cb42c0957 100644 --- a/homeassistant/components/sensor/postnl.py +++ b/homeassistant/components/sensor/postnl.py @@ -59,7 +59,9 @@ class PostNLSensor(Entity): def __init__(self, api, name): """Initialize the PostNL sensor.""" self._name = name - self._attributes = None + self._attributes = { + ATTR_ATTRIBUTION: ATTRIBUTION, + } self._state = None self._api = api @@ -92,18 +94,5 @@ class PostNLSensor(Entity): def update(self): """Update device state.""" shipments = self._api.get_relevant_shipments() - status_counts = {} - - for shipment in shipments: - status = shipment['status']['formatted']['short'] - status = self._api.parse_datetime(status, '%d-%m-%Y', '%H:%M') - - name = shipment['settings']['title'] - status_counts[name] = status - - self._attributes = { - ATTR_ATTRIBUTION: ATTRIBUTION, - **status_counts - } - - self._state = len(status_counts) + self._attributes['shipments'] = shipments + self._state = len(shipments) From 578bfe9798cb007ac014edd3dfb5ac6505ad1462 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 11 Jan 2019 15:01:24 -0800 Subject: [PATCH 117/235] Fix fail2ban tests --- tests/components/sensor/test_fail2ban.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/components/sensor/test_fail2ban.py b/tests/components/sensor/test_fail2ban.py index e7874765950..efc16d5006f 100644 --- a/tests/components/sensor/test_fail2ban.py +++ b/tests/components/sensor/test_fail2ban.py @@ -1,4 +1,5 @@ """The tests for local file sensor platform.""" +from datetime import timedelta import unittest from unittest.mock import Mock, patch @@ -118,7 +119,7 @@ class TestBanSensor(unittest.TestCase): def test_ipv6_ban(self): """Test that log is parsed correctly for IPV6 bans.""" - log_parser = BanLogParser(timedelta(seconds=-1), '/tmp') + log_parser = BanLogParser('/tmp') sensor = BanSensor('fail2ban', 'jail_one', log_parser) assert sensor.name == 'fail2ban jail_one' mock_fh = MockOpen(read_data=fake_log('ipv6_ban')) From 8755389c49b6cdd46563dd1136e72ffff027abcd Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 11 Jan 2019 15:10:01 -0800 Subject: [PATCH 118/235] Bumped version to 0.86.0.dev0 --- homeassistant/const.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 31359167e0f..370a4d82faf 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,8 +1,8 @@ # coding: utf-8 """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 -MINOR_VERSION = 85 -PATCH_VERSION = '1' +MINOR_VERSION = 86 +PATCH_VERSION = '0.dev0' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From d820efc4e377cf1f282de0a4b1bfb81f825f006b Mon Sep 17 00:00:00 2001 From: Rohan Kapoor Date: Fri, 11 Jan 2019 15:14:11 -0800 Subject: [PATCH 119/235] Split locative to a separate component (#19964) * Split locative to a separate component * Switch tests to use constants for http codes * Fix tests --- .../components/device_tracker/locative.py | 107 ++------------- homeassistant/components/locative/__init__.py | 120 +++++++++++++++++ tests/components/locative/__init__.py | 1 + .../test_init.py} | 127 ++++++++++-------- 4 files changed, 202 insertions(+), 153 deletions(-) create mode 100644 homeassistant/components/locative/__init__.py create mode 100644 tests/components/locative/__init__.py rename tests/components/{device_tracker/test_locative.py => locative/test_init.py} (52%) diff --git a/homeassistant/components/device_tracker/locative.py b/homeassistant/components/device_tracker/locative.py index aa91f0d3d71..e7a63077a3a 100644 --- a/homeassistant/components/device_tracker/locative.py +++ b/homeassistant/components/device_tracker/locative.py @@ -4,106 +4,25 @@ Support for the Locative platform. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/device_tracker.locative/ """ -from functools import partial import logging -from homeassistant.const import ( - ATTR_LATITUDE, ATTR_LONGITUDE, STATE_NOT_HOME, HTTP_UNPROCESSABLE_ENTITY) -from homeassistant.components.http import HomeAssistantView -# pylint: disable=unused-import -from homeassistant.components.device_tracker import ( # NOQA - DOMAIN, PLATFORM_SCHEMA) +from homeassistant.components.locative import TRACKER_UPDATE +from homeassistant.helpers.dispatcher import async_dispatcher_connect _LOGGER = logging.getLogger(__name__) -DEPENDENCIES = ['http'] -URL = '/api/locative' +DEPENDENCIES = ['locative'] -def setup_scanner(hass, config, see, discovery_info=None): - """Set up an endpoint for the Locative application.""" - hass.http.register_view(LocativeView(see)) +async def async_setup_scanner(hass, config, async_see, discovery_info=None): + """Set up an endpoint for the Locative device tracker.""" + async def _set_location(device, gps_location, location_name): + """Fire HA event to set location.""" + await async_see( + dev_id=device, + gps=gps_location, + location_name=location_name + ) + async_dispatcher_connect(hass, TRACKER_UPDATE, _set_location) return True - - -class LocativeView(HomeAssistantView): - """View to handle Locative requests.""" - - url = URL - name = 'api:locative' - - def __init__(self, see): - """Initialize Locative URL endpoints.""" - self.see = see - - async def get(self, request): - """Locative message received as GET.""" - res = await self._handle(request.app['hass'], request.query) - return res - - async def post(self, request): - """Locative message received.""" - data = await request.post() - res = await self._handle(request.app['hass'], data) - return res - - async def _handle(self, hass, data): - """Handle locative request.""" - if 'latitude' not in data or 'longitude' not in data: - return ('Latitude and longitude not specified.', - HTTP_UNPROCESSABLE_ENTITY) - - if 'device' not in data: - _LOGGER.error('Device id not specified.') - return ('Device id not specified.', - HTTP_UNPROCESSABLE_ENTITY) - - if 'trigger' not in data: - _LOGGER.error('Trigger is not specified.') - return ('Trigger is not specified.', - HTTP_UNPROCESSABLE_ENTITY) - - if 'id' not in data and data['trigger'] != 'test': - _LOGGER.error('Location id not specified.') - return ('Location id not specified.', - HTTP_UNPROCESSABLE_ENTITY) - - device = data['device'].replace('-', '') - location_name = data.get('id', data['trigger']).lower() - direction = data['trigger'] - gps_location = (data[ATTR_LATITUDE], data[ATTR_LONGITUDE]) - - if direction == 'enter': - await hass.async_add_job( - partial(self.see, dev_id=device, location_name=location_name, - gps=gps_location)) - return 'Setting location to {}'.format(location_name) - - if direction == 'exit': - current_state = hass.states.get( - '{}.{}'.format(DOMAIN, device)) - - if current_state is None or current_state.state == location_name: - location_name = STATE_NOT_HOME - await hass.async_add_job( - partial(self.see, dev_id=device, - location_name=location_name, gps=gps_location)) - return 'Setting location to not home' - - # Ignore the message if it is telling us to exit a zone that we - # aren't currently in. This occurs when a zone is entered - # before the previous zone was exited. The enter message will - # be sent first, then the exit message will be sent second. - return 'Ignoring exit from {} (already in {})'.format( - location_name, current_state) - - if direction == 'test': - # In the app, a test message can be sent. Just return something to - # the user to let them know that it works. - return 'Received test message.' - - _LOGGER.error('Received unidentified message from Locative: %s', - direction) - return ('Received unidentified message: {}'.format(direction), - HTTP_UNPROCESSABLE_ENTITY) diff --git a/homeassistant/components/locative/__init__.py b/homeassistant/components/locative/__init__.py new file mode 100644 index 00000000000..1f924987dd3 --- /dev/null +++ b/homeassistant/components/locative/__init__.py @@ -0,0 +1,120 @@ +""" +Support for Locative. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/locative/ +""" +import logging + +from homeassistant.components.device_tracker import \ + DOMAIN as DEVICE_TRACKER_DOMAIN +from homeassistant.components.http import HomeAssistantView +from homeassistant.const import HTTP_UNPROCESSABLE_ENTITY, ATTR_LATITUDE, \ + ATTR_LONGITUDE, STATE_NOT_HOME +from homeassistant.helpers.discovery import async_load_platform +from homeassistant.helpers.dispatcher import async_dispatcher_send + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = 'locative' +DEPENDENCIES = ['http'] + +URL = '/api/locative' + +TRACKER_UPDATE = '{}_tracker_update'.format(DOMAIN) + + +async def async_setup(hass, hass_config): + """Set up the Locative component.""" + hass.http.register_view(LocativeView) + hass.async_create_task( + async_load_platform(hass, 'device_tracker', DOMAIN, {}, hass_config) + ) + return True + + +class LocativeView(HomeAssistantView): + """View to handle Locative requests.""" + + url = URL + name = 'api:locative' + + def __init__(self): + """Initialize Locative URL endpoints.""" + + async def get(self, request): + """Locative message received as GET.""" + return await self._handle(request.app['hass'], request.query) + + async def post(self, request): + """Locative message received.""" + data = await request.post() + return await self._handle(request.app['hass'], data) + + async def _handle(self, hass, data): + """Handle locative request.""" + if 'latitude' not in data or 'longitude' not in data: + return ('Latitude and longitude not specified.', + HTTP_UNPROCESSABLE_ENTITY) + + if 'device' not in data: + _LOGGER.error('Device id not specified.') + return ('Device id not specified.', + HTTP_UNPROCESSABLE_ENTITY) + + if 'trigger' not in data: + _LOGGER.error('Trigger is not specified.') + return ('Trigger is not specified.', + HTTP_UNPROCESSABLE_ENTITY) + + if 'id' not in data and data['trigger'] != 'test': + _LOGGER.error('Location id not specified.') + return ('Location id not specified.', + HTTP_UNPROCESSABLE_ENTITY) + + device = data['device'].replace('-', '') + location_name = data.get('id', data['trigger']).lower() + direction = data['trigger'] + gps_location = (data[ATTR_LATITUDE], data[ATTR_LONGITUDE]) + + if direction == 'enter': + async_dispatcher_send( + hass, + TRACKER_UPDATE, + device, + gps_location, + location_name + ) + return 'Setting location to {}'.format(location_name) + + if direction == 'exit': + current_state = hass.states.get( + '{}.{}'.format(DEVICE_TRACKER_DOMAIN, device)) + + if current_state is None or current_state.state == location_name: + location_name = STATE_NOT_HOME + async_dispatcher_send( + hass, + TRACKER_UPDATE, + device, + gps_location, + location_name + ) + return 'Setting location to not home' + + # Ignore the message if it is telling us to exit a zone that we + # aren't currently in. This occurs when a zone is entered + # before the previous zone was exited. The enter message will + # be sent first, then the exit message will be sent second. + return 'Ignoring exit from {} (already in {})'.format( + location_name, current_state) + + if direction == 'test': + # In the app, a test message can be sent. Just return something to + # the user to let them know that it works. + return 'Received test message.' + + _LOGGER.error('Received unidentified message from Locative: %s', + direction) + return ('Received unidentified message: {}'.format(direction), + HTTP_UNPROCESSABLE_ENTITY) diff --git a/tests/components/locative/__init__.py b/tests/components/locative/__init__.py new file mode 100644 index 00000000000..8be6da6628d --- /dev/null +++ b/tests/components/locative/__init__.py @@ -0,0 +1 @@ +"""Tests for the Locative component.""" diff --git a/tests/components/device_tracker/test_locative.py b/tests/components/locative/test_init.py similarity index 52% rename from tests/components/device_tracker/test_locative.py rename to tests/components/locative/test_init.py index a167a1e9fd4..50c72f468af 100644 --- a/tests/components/device_tracker/test_locative.py +++ b/tests/components/locative/test_init.py @@ -1,13 +1,13 @@ """The tests the for Locative device tracker platform.""" -import asyncio from unittest.mock import patch import pytest +from homeassistant.components.device_tracker import \ + DOMAIN as DEVICE_TRACKER_DOMAIN +from homeassistant.components.locative import URL, DOMAIN +from homeassistant.const import HTTP_OK, HTTP_UNPROCESSABLE_ENTITY from homeassistant.setup import async_setup_component -import homeassistant.components.device_tracker as device_tracker -from homeassistant.const import CONF_PLATFORM -from homeassistant.components.device_tracker.locative import URL def _url(data=None): @@ -18,22 +18,25 @@ def _url(data=None): return "{}?{}".format(URL, data) +@pytest.fixture(autouse=True) +def mock_dev_track(mock_device_tracker_conf): + """Mock device tracker config loading.""" + pass + + @pytest.fixture def locative_client(loop, hass, hass_client): """Locative mock client.""" assert loop.run_until_complete(async_setup_component( - hass, device_tracker.DOMAIN, { - device_tracker.DOMAIN: { - CONF_PLATFORM: 'locative' - } + hass, DOMAIN, { + DOMAIN: {} })) with patch('homeassistant.components.device_tracker.update_config'): yield loop.run_until_complete(hass_client()) -@asyncio.coroutine -def test_missing_data(locative_client): +async def test_missing_data(locative_client): """Test missing data.""" data = { 'latitude': 1.0, @@ -44,55 +47,54 @@ def test_missing_data(locative_client): } # No data - req = yield from locative_client.get(_url({})) - assert req.status == 422 + req = await locative_client.get(_url({})) + assert req.status == HTTP_UNPROCESSABLE_ENTITY # No latitude copy = data.copy() del copy['latitude'] - req = yield from locative_client.get(_url(copy)) - assert req.status == 422 + req = await locative_client.get(_url(copy)) + assert req.status == HTTP_UNPROCESSABLE_ENTITY # No device copy = data.copy() del copy['device'] - req = yield from locative_client.get(_url(copy)) - assert req.status == 422 + req = await locative_client.get(_url(copy)) + assert req.status == HTTP_UNPROCESSABLE_ENTITY # No location copy = data.copy() del copy['id'] - req = yield from locative_client.get(_url(copy)) - assert req.status == 422 + req = await locative_client.get(_url(copy)) + assert req.status == HTTP_UNPROCESSABLE_ENTITY # No trigger copy = data.copy() del copy['trigger'] - req = yield from locative_client.get(_url(copy)) - assert req.status == 422 + req = await locative_client.get(_url(copy)) + assert req.status == HTTP_UNPROCESSABLE_ENTITY # Test message copy = data.copy() copy['trigger'] = 'test' - req = yield from locative_client.get(_url(copy)) - assert req.status == 200 + req = await locative_client.get(_url(copy)) + assert req.status == HTTP_OK # Test message, no location copy = data.copy() copy['trigger'] = 'test' del copy['id'] - req = yield from locative_client.get(_url(copy)) - assert req.status == 200 + req = await locative_client.get(_url(copy)) + assert req.status == HTTP_OK # Unknown trigger copy = data.copy() copy['trigger'] = 'foobar' - req = yield from locative_client.get(_url(copy)) - assert req.status == 422 + req = await locative_client.get(_url(copy)) + assert req.status == HTTP_UNPROCESSABLE_ENTITY -@asyncio.coroutine -def test_enter_and_exit(hass, locative_client): +async def test_enter_and_exit(hass, locative_client): """Test when there is a known zone.""" data = { 'latitude': 40.7855, @@ -103,9 +105,10 @@ def test_enter_and_exit(hass, locative_client): } # Enter the Home - req = yield from locative_client.get(_url(data)) - assert req.status == 200 - state_name = hass.states.get('{}.{}'.format('device_tracker', + req = await locative_client.get(_url(data)) + await hass.async_block_till_done() + assert req.status == HTTP_OK + state_name = hass.states.get('{}.{}'.format(DEVICE_TRACKER_DOMAIN, data['device'])).state assert 'home' == state_name @@ -113,9 +116,10 @@ def test_enter_and_exit(hass, locative_client): data['trigger'] = 'exit' # Exit Home - req = yield from locative_client.get(_url(data)) - assert req.status == 200 - state_name = hass.states.get('{}.{}'.format('device_tracker', + req = await locative_client.get(_url(data)) + await hass.async_block_till_done() + assert req.status == HTTP_OK + state_name = hass.states.get('{}.{}'.format(DEVICE_TRACKER_DOMAIN, data['device'])).state assert 'not_home' == state_name @@ -123,18 +127,20 @@ def test_enter_and_exit(hass, locative_client): data['trigger'] = 'enter' # Enter Home again - req = yield from locative_client.get(_url(data)) - assert req.status == 200 - state_name = hass.states.get('{}.{}'.format('device_tracker', + req = await locative_client.get(_url(data)) + await hass.async_block_till_done() + assert req.status == HTTP_OK + state_name = hass.states.get('{}.{}'.format(DEVICE_TRACKER_DOMAIN, data['device'])).state assert 'home' == state_name data['trigger'] = 'exit' # Exit Home - req = yield from locative_client.get(_url(data)) - assert req.status == 200 - state_name = hass.states.get('{}.{}'.format('device_tracker', + req = await locative_client.get(_url(data)) + await hass.async_block_till_done() + assert req.status == HTTP_OK + state_name = hass.states.get('{}.{}'.format(DEVICE_TRACKER_DOMAIN, data['device'])).state assert 'not_home' == state_name @@ -142,15 +148,15 @@ def test_enter_and_exit(hass, locative_client): data['trigger'] = 'enter' # Enter Work - req = yield from locative_client.get(_url(data)) - assert req.status == 200 - state_name = hass.states.get('{}.{}'.format('device_tracker', + req = await locative_client.get(_url(data)) + await hass.async_block_till_done() + assert req.status == HTTP_OK + state_name = hass.states.get('{}.{}'.format(DEVICE_TRACKER_DOMAIN, data['device'])).state assert 'work' == state_name -@asyncio.coroutine -def test_exit_after_enter(hass, locative_client): +async def test_exit_after_enter(hass, locative_client): """Test when an exit message comes after an enter message.""" data = { 'latitude': 40.7855, @@ -161,20 +167,22 @@ def test_exit_after_enter(hass, locative_client): } # Enter Home - req = yield from locative_client.get(_url(data)) - assert req.status == 200 + req = await locative_client.get(_url(data)) + await hass.async_block_till_done() + assert req.status == HTTP_OK - state = hass.states.get('{}.{}'.format('device_tracker', + state = hass.states.get('{}.{}'.format(DEVICE_TRACKER_DOMAIN, data['device'])) assert state.state == 'home' data['id'] = 'Work' # Enter Work - req = yield from locative_client.get(_url(data)) - assert req.status == 200 + req = await locative_client.get(_url(data)) + await hass.async_block_till_done() + assert req.status == HTTP_OK - state = hass.states.get('{}.{}'.format('device_tracker', + state = hass.states.get('{}.{}'.format(DEVICE_TRACKER_DOMAIN, data['device'])) assert state.state == 'work' @@ -182,16 +190,16 @@ def test_exit_after_enter(hass, locative_client): data['trigger'] = 'exit' # Exit Home - req = yield from locative_client.get(_url(data)) - assert req.status == 200 + req = await locative_client.get(_url(data)) + await hass.async_block_till_done() + assert req.status == HTTP_OK - state = hass.states.get('{}.{}'.format('device_tracker', + state = hass.states.get('{}.{}'.format(DEVICE_TRACKER_DOMAIN, data['device'])) assert state.state == 'work' -@asyncio.coroutine -def test_exit_first(hass, locative_client): +async def test_exit_first(hass, locative_client): """Test when an exit message is sent first on a new device.""" data = { 'latitude': 40.7855, @@ -202,9 +210,10 @@ def test_exit_first(hass, locative_client): } # Exit Home - req = yield from locative_client.get(_url(data)) - assert req.status == 200 + req = await locative_client.get(_url(data)) + await hass.async_block_till_done() + assert req.status == HTTP_OK - state = hass.states.get('{}.{}'.format('device_tracker', + state = hass.states.get('{}.{}'.format(DEVICE_TRACKER_DOMAIN, data['device'])) assert state.state == 'not_home' From 7dac7b9e5e83809d2a42e5d148d088935a8ceb16 Mon Sep 17 00:00:00 2001 From: pbalogh77 Date: Sat, 12 Jan 2019 00:29:54 +0100 Subject: [PATCH 120/235] Support for multiple Fibaro gateways (#19705) * Preparing for transition to config flow Added multiple gateway support Reworked parameter flow to platforms to enable multiple controllers Breaking change to config, now a list of gateways is expected instead of a single config * Updated coveragerc Added new location of fibaro component * Fixes based on code review and extended logging Addressed issues raised by code review Added extended debug logging to get better reports from users if the device type mapping is not perfect * Changhes based on code review Changes to how configuration is read and schemas Fix to device type mapping logic * simplified reading config * oops oops * grr grr * change based on code review * changes based on code review changes based on code review --- .coveragerc | 2 +- .../components/binary_sensor/fibaro.py | 8 +- homeassistant/components/cover/fibaro.py | 8 +- .../{fibaro.py => fibaro/__init__.py} | 90 +++++++++++-------- homeassistant/components/light/fibaro.py | 8 +- homeassistant/components/scene/fibaro.py | 4 +- homeassistant/components/sensor/fibaro.py | 8 +- homeassistant/components/switch/fibaro.py | 8 +- 8 files changed, 78 insertions(+), 58 deletions(-) rename homeassistant/components/{fibaro.py => fibaro/__init__.py} (85%) diff --git a/.coveragerc b/.coveragerc index 3107af9140a..f062501bd35 100644 --- a/.coveragerc +++ b/.coveragerc @@ -127,7 +127,7 @@ omit = homeassistant/components/eufy.py homeassistant/components/*/eufy.py - homeassistant/components/fibaro.py + homeassistant/components/fibaro/__init__.py homeassistant/components/*/fibaro.py homeassistant/components/gc100.py diff --git a/homeassistant/components/binary_sensor/fibaro.py b/homeassistant/components/binary_sensor/fibaro.py index 8af2bde10ad..1934580c58e 100644 --- a/homeassistant/components/binary_sensor/fibaro.py +++ b/homeassistant/components/binary_sensor/fibaro.py @@ -9,7 +9,7 @@ import logging from homeassistant.components.binary_sensor import ( BinarySensorDevice, ENTITY_ID_FORMAT) from homeassistant.components.fibaro import ( - FIBARO_CONTROLLER, FIBARO_DEVICES, FibaroDevice) + FIBARO_DEVICES, FibaroDevice) from homeassistant.const import (CONF_DEVICE_CLASS, CONF_ICON) DEPENDENCIES = ['fibaro'] @@ -33,17 +33,17 @@ def setup_platform(hass, config, add_entities, discovery_info=None): return add_entities( - [FibaroBinarySensor(device, hass.data[FIBARO_CONTROLLER]) + [FibaroBinarySensor(device) for device in hass.data[FIBARO_DEVICES]['binary_sensor']], True) class FibaroBinarySensor(FibaroDevice, BinarySensorDevice): """Representation of a Fibaro Binary Sensor.""" - def __init__(self, fibaro_device, controller): + def __init__(self, fibaro_device): """Initialize the binary_sensor.""" self._state = None - super().__init__(fibaro_device, controller) + super().__init__(fibaro_device) self.entity_id = ENTITY_ID_FORMAT.format(self.ha_id) stype = None devconf = fibaro_device.device_config diff --git a/homeassistant/components/cover/fibaro.py b/homeassistant/components/cover/fibaro.py index dc82087f802..d47dbb20315 100644 --- a/homeassistant/components/cover/fibaro.py +++ b/homeassistant/components/cover/fibaro.py @@ -9,7 +9,7 @@ import logging from homeassistant.components.cover import ( CoverDevice, ENTITY_ID_FORMAT, ATTR_POSITION, ATTR_TILT_POSITION) from homeassistant.components.fibaro import ( - FIBARO_CONTROLLER, FIBARO_DEVICES, FibaroDevice) + FIBARO_DEVICES, FibaroDevice) DEPENDENCIES = ['fibaro'] @@ -22,16 +22,16 @@ def setup_platform(hass, config, add_entities, discovery_info=None): return add_entities( - [FibaroCover(device, hass.data[FIBARO_CONTROLLER]) for + [FibaroCover(device) for device in hass.data[FIBARO_DEVICES]['cover']], True) class FibaroCover(FibaroDevice, CoverDevice): """Representation a Fibaro Cover.""" - def __init__(self, fibaro_device, controller): + def __init__(self, fibaro_device): """Initialize the Vera device.""" - super().__init__(fibaro_device, controller) + super().__init__(fibaro_device) self.entity_id = ENTITY_ID_FORMAT.format(self.ha_id) @staticmethod diff --git a/homeassistant/components/fibaro.py b/homeassistant/components/fibaro/__init__.py similarity index 85% rename from homeassistant/components/fibaro.py rename to homeassistant/components/fibaro/__init__.py index d506f6c471d..715cc036265 100644 --- a/homeassistant/components/fibaro.py +++ b/homeassistant/components/fibaro/__init__.py @@ -11,8 +11,8 @@ from typing import Optional import voluptuous as vol from homeassistant.const import ( - ATTR_ARMED, ATTR_BATTERY_LEVEL, CONF_DEVICE_CLASS, - CONF_EXCLUDE, CONF_ICON, CONF_PASSWORD, CONF_URL, CONF_USERNAME, + ATTR_ARMED, ATTR_BATTERY_LEVEL, CONF_DEVICE_CLASS, CONF_EXCLUDE, + CONF_ICON, CONF_PASSWORD, CONF_URL, CONF_USERNAME, CONF_WHITE_VALUE, EVENT_HOMEASSISTANT_STOP) from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv @@ -24,10 +24,11 @@ REQUIREMENTS = ['fiblary3==0.1.7'] _LOGGER = logging.getLogger(__name__) DOMAIN = 'fibaro' FIBARO_DEVICES = 'fibaro_devices' -FIBARO_CONTROLLER = 'fibaro_controller' +FIBARO_CONTROLLERS = 'fibaro_controllers' ATTR_CURRENT_POWER_W = "current_power_w" ATTR_CURRENT_ENERGY_KWH = "current_energy_kwh" CONF_PLUGINS = "plugins" +CONF_GATEWAYS = 'gateways' CONF_DIMMING = "dimming" CONF_COLOR = "color" CONF_RESET_COLOR = "reset_color" @@ -65,15 +66,20 @@ DEVICE_CONFIG_SCHEMA_ENTRY = vol.Schema({ FIBARO_ID_LIST_SCHEMA = vol.Schema([cv.string]) +GATEWAY_CONFIG = vol.Schema({ + vol.Required(CONF_PASSWORD): cv.string, + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_URL): cv.url, + vol.Optional(CONF_PLUGINS, default=False): cv.boolean, + vol.Optional(CONF_EXCLUDE, default=[]): FIBARO_ID_LIST_SCHEMA, + vol.Optional(CONF_DEVICE_CONFIG, default={}): + vol.Schema({cv.string: DEVICE_CONFIG_SCHEMA_ENTRY}) +}, extra=vol.ALLOW_EXTRA) + CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ - vol.Required(CONF_PASSWORD): cv.string, - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_URL): cv.url, - vol.Optional(CONF_PLUGINS, default=False): cv.boolean, - vol.Optional(CONF_EXCLUDE, default=[]): FIBARO_ID_LIST_SCHEMA, - vol.Optional(CONF_DEVICE_CONFIG, default={}): - vol.Schema({cv.string: DEVICE_CONFIG_SCHEMA_ENTRY}) + vol.Required(CONF_GATEWAYS): + vol.All(cv.ensure_list, [GATEWAY_CONFIG]) }) }, extra=vol.ALLOW_EXTRA) @@ -81,20 +87,23 @@ CONFIG_SCHEMA = vol.Schema({ class FibaroController(): """Initiate Fibaro Controller Class.""" - def __init__(self, username, password, url, import_plugins, config): + def __init__(self, config): """Initialize the Fibaro controller.""" from fiblary3.client.v4.client import Client as FibaroClient - self._client = FibaroClient(url, username, password) + + self._client = FibaroClient(config[CONF_URL], + config[CONF_USERNAME], + config[CONF_PASSWORD]) self._scene_map = None # Whether to import devices from plugins - self._import_plugins = import_plugins + self._import_plugins = config[CONF_PLUGINS] self._device_config = config[CONF_DEVICE_CONFIG] self._room_map = None # Mapping roomId to room object self._device_map = None # Mapping deviceId to device object self.fibaro_devices = None # List of devices by type self._callbacks = {} # Update value callbacks by deviceId self._state_handler = None # Fiblary's StateHandler object - self._excluded_devices = config.get(CONF_EXCLUDE, []) + self._excluded_devices = config[CONF_EXCLUDE] self.hub_serial = None # Unique serial number of the hub def connect(self): @@ -167,12 +176,11 @@ class FibaroController(): def _map_device_to_type(device): """Map device to HA device type.""" # Use our lookup table to identify device type + device_type = None if 'type' in device: device_type = FIBARO_TYPEMAP.get(device.type) - elif 'baseType' in device: + if device_type is None and 'baseType' in device: device_type = FIBARO_TYPEMAP.get(device.baseType) - else: - device_type = None # We can also identify device type by its capabilities if device_type is None: @@ -200,6 +208,7 @@ class FibaroController(): for device in scenes: if not device.visible: continue + device.fibaro_controller = self if device.roomID == 0: room_name = 'Unknown' else: @@ -220,6 +229,7 @@ class FibaroController(): self.fibaro_devices = defaultdict(list) for device in devices: try: + device.fibaro_controller = self if device.roomID == 0: room_name = 'Unknown' else: @@ -242,33 +252,43 @@ class FibaroController(): self.hub_serial, device.id) self._device_map[device.id] = device self.fibaro_devices[device.mapped_type].append(device) - else: - _LOGGER.debug("%s (%s, %s) not used", - device.ha_id, device.type, - device.baseType) + _LOGGER.debug("%s (%s, %s) -> %s. Prop: %s Actions: %s", + device.ha_id, device.type, + device.baseType, device.mapped_type, + str(device.properties), str(device.actions)) except (KeyError, ValueError): pass -def setup(hass, config): +def setup(hass, base_config): """Set up the Fibaro Component.""" - hass.data[FIBARO_CONTROLLER] = controller = \ - FibaroController(config[DOMAIN][CONF_USERNAME], - config[DOMAIN][CONF_PASSWORD], - config[DOMAIN][CONF_URL], - config[DOMAIN][CONF_PLUGINS], - config[DOMAIN]) + gateways = base_config[DOMAIN][CONF_GATEWAYS] + hass.data[FIBARO_CONTROLLERS] = {} def stop_fibaro(event): """Stop Fibaro Thread.""" _LOGGER.info("Shutting down Fibaro connection") - hass.data[FIBARO_CONTROLLER].disable_state_handler() + for controller in hass.data[FIBARO_CONTROLLERS].values(): + controller.disable_state_handler() - if controller.connect(): - hass.data[FIBARO_DEVICES] = controller.fibaro_devices + hass.data[FIBARO_DEVICES] = {} + for component in FIBARO_COMPONENTS: + hass.data[FIBARO_DEVICES][component] = [] + + for gateway in gateways: + controller = FibaroController(gateway) + if controller.connect(): + hass.data[FIBARO_CONTROLLERS][controller.hub_serial] = controller + for component in FIBARO_COMPONENTS: + hass.data[FIBARO_DEVICES][component].extend( + controller.fibaro_devices[component]) + + if hass.data[FIBARO_CONTROLLERS]: for component in FIBARO_COMPONENTS: - discovery.load_platform(hass, component, DOMAIN, {}, config) - controller.enable_state_handler() + discovery.load_platform(hass, component, DOMAIN, {}, + base_config) + for controller in hass.data[FIBARO_CONTROLLERS].values(): + controller.enable_state_handler() hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_fibaro) return True @@ -278,10 +298,10 @@ def setup(hass, config): class FibaroDevice(Entity): """Representation of a Fibaro device entity.""" - def __init__(self, fibaro_device, controller): + def __init__(self, fibaro_device): """Initialize the device.""" self.fibaro_device = fibaro_device - self.controller = controller + self.controller = fibaro_device.fibaro_controller self._name = fibaro_device.friendly_name self.ha_id = fibaro_device.ha_id diff --git a/homeassistant/components/light/fibaro.py b/homeassistant/components/light/fibaro.py index 6f2842524a2..9b3b3850f39 100644 --- a/homeassistant/components/light/fibaro.py +++ b/homeassistant/components/light/fibaro.py @@ -12,7 +12,7 @@ from functools import partial from homeassistant.const import ( CONF_WHITE_VALUE) from homeassistant.components.fibaro import ( - FIBARO_CONTROLLER, FIBARO_DEVICES, FibaroDevice, + FIBARO_DEVICES, FibaroDevice, CONF_DIMMING, CONF_COLOR, CONF_RESET_COLOR) from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_HS_COLOR, ATTR_WHITE_VALUE, ENTITY_ID_FORMAT, @@ -50,14 +50,14 @@ async def async_setup_platform(hass, return async_add_entities( - [FibaroLight(device, hass.data[FIBARO_CONTROLLER]) + [FibaroLight(device) for device in hass.data[FIBARO_DEVICES]['light']], True) class FibaroLight(FibaroDevice, Light): """Representation of a Fibaro Light, including dimmable.""" - def __init__(self, fibaro_device, controller): + def __init__(self, fibaro_device): """Initialize the light.""" self._brightness = None self._color = (0, 0) @@ -81,7 +81,7 @@ class FibaroLight(FibaroDevice, Light): if devconf.get(CONF_WHITE_VALUE, supports_white_v): self._supported_flags |= SUPPORT_WHITE_VALUE - super().__init__(fibaro_device, controller) + super().__init__(fibaro_device) self.entity_id = ENTITY_ID_FORMAT.format(self.ha_id) @property diff --git a/homeassistant/components/scene/fibaro.py b/homeassistant/components/scene/fibaro.py index 7a36900f884..a0bd4e7ff40 100644 --- a/homeassistant/components/scene/fibaro.py +++ b/homeassistant/components/scene/fibaro.py @@ -9,7 +9,7 @@ import logging from homeassistant.components.scene import ( Scene) from homeassistant.components.fibaro import ( - FIBARO_CONTROLLER, FIBARO_DEVICES, FibaroDevice) + FIBARO_DEVICES, FibaroDevice) DEPENDENCIES = ['fibaro'] @@ -23,7 +23,7 @@ async def async_setup_platform(hass, config, async_add_entities, return async_add_entities( - [FibaroScene(scene, hass.data[FIBARO_CONTROLLER]) + [FibaroScene(scene) for scene in hass.data[FIBARO_DEVICES]['scene']], True) diff --git a/homeassistant/components/sensor/fibaro.py b/homeassistant/components/sensor/fibaro.py index e5ed5638c5b..e437ef8710d 100644 --- a/homeassistant/components/sensor/fibaro.py +++ b/homeassistant/components/sensor/fibaro.py @@ -12,7 +12,7 @@ from homeassistant.const import ( from homeassistant.helpers.entity import Entity from homeassistant.components.sensor import ENTITY_ID_FORMAT from homeassistant.components.fibaro import ( - FIBARO_CONTROLLER, FIBARO_DEVICES, FibaroDevice) + FIBARO_DEVICES, FibaroDevice) SENSOR_TYPES = { 'com.fibaro.temperatureSensor': @@ -37,18 +37,18 @@ def setup_platform(hass, config, add_entities, discovery_info=None): return add_entities( - [FibaroSensor(device, hass.data[FIBARO_CONTROLLER]) + [FibaroSensor(device) for device in hass.data[FIBARO_DEVICES]['sensor']], True) class FibaroSensor(FibaroDevice, Entity): """Representation of a Fibaro Sensor.""" - def __init__(self, fibaro_device, controller): + def __init__(self, fibaro_device): """Initialize the sensor.""" self.current_value = None self.last_changed_time = None - super().__init__(fibaro_device, controller) + super().__init__(fibaro_device) self.entity_id = ENTITY_ID_FORMAT.format(self.ha_id) if fibaro_device.type in SENSOR_TYPES: self._unit = SENSOR_TYPES[fibaro_device.type][1] diff --git a/homeassistant/components/switch/fibaro.py b/homeassistant/components/switch/fibaro.py index d3e96646a45..8b59aabec72 100644 --- a/homeassistant/components/switch/fibaro.py +++ b/homeassistant/components/switch/fibaro.py @@ -9,7 +9,7 @@ import logging from homeassistant.util import convert from homeassistant.components.switch import ENTITY_ID_FORMAT, SwitchDevice from homeassistant.components.fibaro import ( - FIBARO_CONTROLLER, FIBARO_DEVICES, FibaroDevice) + FIBARO_DEVICES, FibaroDevice) DEPENDENCIES = ['fibaro'] _LOGGER = logging.getLogger(__name__) @@ -21,17 +21,17 @@ def setup_platform(hass, config, add_entities, discovery_info=None): return add_entities( - [FibaroSwitch(device, hass.data[FIBARO_CONTROLLER]) for + [FibaroSwitch(device) for device in hass.data[FIBARO_DEVICES]['switch']], True) class FibaroSwitch(FibaroDevice, SwitchDevice): """Representation of a Fibaro Switch.""" - def __init__(self, fibaro_device, controller): + def __init__(self, fibaro_device): """Initialize the Fibaro device.""" self._state = False - super().__init__(fibaro_device, controller) + super().__init__(fibaro_device) self.entity_id = ENTITY_ID_FORMAT.format(self.ha_id) def turn_on(self, **kwargs): From 83c5dc67f77da52b23c550c76f9675c19aaf0806 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sat, 12 Jan 2019 02:24:31 +0200 Subject: [PATCH 121/235] Upgrade huawei-lte-api to 1.1.3 (#19987) --- homeassistant/components/huawei_lte.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/huawei_lte.py b/homeassistant/components/huawei_lte.py index 1f35f86ccb2..9d223df3344 100644 --- a/homeassistant/components/huawei_lte.py +++ b/homeassistant/components/huawei_lte.py @@ -25,7 +25,7 @@ _LOGGER = logging.getLogger(__name__) # https://github.com/quandyfactory/dicttoxml/issues/60 logging.getLogger('dicttoxml').setLevel(logging.WARNING) -REQUIREMENTS = ['huawei-lte-api==1.1.1'] +REQUIREMENTS = ['huawei-lte-api==1.1.3'] MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=10) diff --git a/requirements_all.txt b/requirements_all.txt index 2adbfcbdde2..d0c81e01d15 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -530,7 +530,7 @@ homematicip==0.10.3 httplib2==0.10.3 # homeassistant.components.huawei_lte -huawei-lte-api==1.1.1 +huawei-lte-api==1.1.3 # homeassistant.components.hydrawise hydrawiser==0.1.1 From 574669bd20e1ba3648c6dc898ac85f903cc36f3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sat, 12 Jan 2019 02:24:59 +0200 Subject: [PATCH 122/235] Upgrade pytest-cov to 2.6.1 (#19988) --- 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 ffc56cdb582..e8775ea57c8 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -10,7 +10,7 @@ mypy==0.650 pydocstyle==3.0.0 pylint==2.2.2 pytest-aiohttp==0.3.0 -pytest-cov==2.6.0 +pytest-cov==2.6.1 pytest-sugar==0.9.2 pytest-timeout==1.3.3 pytest==4.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d1ca93ae80f..5f761333e31 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -11,7 +11,7 @@ mypy==0.650 pydocstyle==3.0.0 pylint==2.2.2 pytest-aiohttp==0.3.0 -pytest-cov==2.6.0 +pytest-cov==2.6.1 pytest-sugar==0.9.2 pytest-timeout==1.3.3 pytest==4.0.2 From fe148606b8c76ad7ea658bdad064ab9f8f426f5f Mon Sep 17 00:00:00 2001 From: Louis-Etienne Date: Fri, 11 Jan 2019 19:30:31 -0500 Subject: [PATCH 123/235] Wink: Update pubnubsub-handler version to make it compatible with python 3.7 (#19625) --- homeassistant/components/wink/__init__.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/wink/__init__.py b/homeassistant/components/wink/__init__.py index c4cefa2c2d1..90e717efd9c 100644 --- a/homeassistant/components/wink/__init__.py +++ b/homeassistant/components/wink/__init__.py @@ -25,7 +25,7 @@ from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.event import track_time_interval from homeassistant.util.json import load_json, save_json -REQUIREMENTS = ['python-wink==1.10.1', 'pubnubsub-handler==1.0.2'] +REQUIREMENTS = ['python-wink==1.10.1', 'pubnubsub-handler==1.0.3'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index d0c81e01d15..51cbc19a7e4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -831,7 +831,7 @@ protobuf==3.6.1 psutil==5.4.8 # homeassistant.components.wink -pubnubsub-handler==1.0.2 +pubnubsub-handler==1.0.3 # homeassistant.components.notify.pushbullet # homeassistant.components.sensor.pushbullet From 6cba51fd0edb2fdd9c684ca2bba34cb588a39dff Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 11 Jan 2019 16:31:16 -0800 Subject: [PATCH 124/235] Lint --- tests/components/sensor/test_fail2ban.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/components/sensor/test_fail2ban.py b/tests/components/sensor/test_fail2ban.py index efc16d5006f..f9be2cbf985 100644 --- a/tests/components/sensor/test_fail2ban.py +++ b/tests/components/sensor/test_fail2ban.py @@ -1,5 +1,4 @@ """The tests for local file sensor platform.""" -from datetime import timedelta import unittest from unittest.mock import Mock, patch From 5ab3c7b765c8b0556d554ee38fcd32795bfc0a63 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Fri, 11 Jan 2019 21:41:27 -0500 Subject: [PATCH 125/235] Don't set friendly_name in Zha entity. (#19991) Use @property name instead of setting friendly_name device state attr. --- .../components/zha/entities/entity.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/zha/entities/entity.py b/homeassistant/components/zha/entities/entity.py index 2b8ee2f1748..9247d6eeb00 100644 --- a/homeassistant/components/zha/entities/entity.py +++ b/homeassistant/components/zha/entities/entity.py @@ -13,7 +13,7 @@ from homeassistant.components.zha.const import ( ATTR_VALUE, ATTR_MANUFACTURER, ATTR_COMMAND, SERVER, ATTR_COMMAND_TYPE, ATTR_ARGS, IN, OUT, CLIENT_COMMANDS, SERVER_COMMANDS) from homeassistant.components.zha.helpers import bind_configure_reporting -from homeassistant.const import ATTR_ENTITY_ID, CONF_FRIENDLY_NAME +from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import callback from homeassistant.helpers import entity from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE @@ -34,6 +34,7 @@ class ZhaEntity(entity.Entity): **kwargs): """Init ZHA entity.""" self._device_state_attributes = {} + self._name = None ieee = endpoint.device.ieee ieeetail = ''.join(['%02x' % (o, ) for o in ieee[-4:]]) if manufacturer and model is not None: @@ -45,10 +46,7 @@ class ZhaEntity(entity.Entity): endpoint.endpoint_id, kwargs.get(ENTITY_SUFFIX, ''), ) - self._device_state_attributes[CONF_FRIENDLY_NAME] = "{} {}".format( - manufacturer, - model, - ) + self._name = "{} {}".format(manufacturer, model) else: self.entity_id = "{}.zha_{}_{}{}".format( self._domain, @@ -233,6 +231,11 @@ class ZhaEntity(entity.Entity): cluster = self._out_clusters[cluster_id] return cluster + @property + def name(self): + """Return Entity's default name.""" + return self._name + @property def zcl_reporting_config(self): """Return a dict of ZCL attribute reporting configuration. @@ -302,10 +305,7 @@ class ZhaEntity(entity.Entity): 'identifiers': {(DOMAIN, ieee)}, ATTR_MANUFACTURER: self._endpoint.manufacturer, 'model': self._endpoint.model, - 'name': self._device_state_attributes.get( - CONF_FRIENDLY_NAME, - ieee - ), + 'name': self.name or ieee, 'via_hub': (DOMAIN, self.hass.data[DATA_ZHA][DATA_ZHA_BRIDGE_ID]), } From 7676b3fbe8efde90134b89e0fb1d1154e8fd74cb Mon Sep 17 00:00:00 2001 From: Austin Drummond Date: Fri, 11 Jan 2019 21:48:28 -0500 Subject: [PATCH 126/235] Add support for HomeKit Controller Locks (#19867) * Added HomeKit Controller Lock * cleaned up code according to standards * fixed lint issues * added private constant for jammed state * removed state_unknown --- .../components/homekit_controller/__init__.py | 2 + .../components/lock/homekit_controller.py | 111 ++++++++++++++++++ 2 files changed, 113 insertions(+) create mode 100644 homeassistant/components/lock/homekit_controller.py diff --git a/homeassistant/components/homekit_controller/__init__.py b/homeassistant/components/homekit_controller/__init__.py index 74fbb228f54..a8264e6a080 100644 --- a/homeassistant/components/homekit_controller/__init__.py +++ b/homeassistant/components/homekit_controller/__init__.py @@ -25,6 +25,7 @@ HOMEKIT_ACCESSORY_DISPATCH = { 'switch': 'switch', 'thermostat': 'climate', 'security-system': 'alarm_control_panel', + 'lock-mechanism': 'lock' } HOMEKIT_IGNORE = [ @@ -116,6 +117,7 @@ class HKDevice(): for service in accessory['services']: service_info = {'serial': serial, 'aid': aid, + 'model': self.model, 'iid': service['iid']} devtype = ServicesTypes.get_short(service['type']) _LOGGER.debug("Found %s", devtype) diff --git a/homeassistant/components/lock/homekit_controller.py b/homeassistant/components/lock/homekit_controller.py new file mode 100644 index 00000000000..910567ed182 --- /dev/null +++ b/homeassistant/components/lock/homekit_controller.py @@ -0,0 +1,111 @@ +""" +Support for HomeKit Controller locks. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/lock.homekit_controller/ +""" + +import logging + +from homeassistant.components.homekit_controller import (HomeKitEntity, + KNOWN_ACCESSORIES) +from homeassistant.components.lock import LockDevice +from homeassistant.const import (STATE_LOCKED, STATE_UNLOCKED, + ATTR_BATTERY_LEVEL) + +DEPENDENCIES = ['homekit_controller'] + +_LOGGER = logging.getLogger(__name__) + +STATE_JAMMED = 'jammed' + +CURRENT_STATE_MAP = { + 0: STATE_UNLOCKED, + 1: STATE_LOCKED, + 2: STATE_JAMMED, + 3: None, +} + +TARGET_STATE_MAP = { + STATE_UNLOCKED: 0, + STATE_LOCKED: 1 +} + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up Homekit Lock support.""" + if discovery_info is None: + return + accessory = hass.data[KNOWN_ACCESSORIES][discovery_info['serial']] + add_entities([HomeKitLock(accessory, discovery_info)], + True) + + +class HomeKitLock(HomeKitEntity, LockDevice): + """Representation of a HomeKit Controller Lock.""" + + def __init__(self, accessory, discovery_info): + """Initialise the Lock.""" + super().__init__(accessory, discovery_info) + self._state = None + self._name = discovery_info['model'] + self._battery_level = None + + def update_characteristics(self, characteristics): + """Synchronise the Lock state with Home Assistant.""" + # pylint: disable=import-error + from homekit.model.characteristics import CharacteristicsTypes + + for characteristic in characteristics: + ctype = characteristic['type'] + ctype = CharacteristicsTypes.get_short(ctype) + if ctype == "lock-mechanism.current-state": + self._chars['lock-mechanism.current-state'] = \ + characteristic['iid'] + self._state = CURRENT_STATE_MAP[characteristic['value']] + elif ctype == "lock-mechanism.target-state": + self._chars['lock-mechanism.target-state'] = \ + characteristic['iid'] + elif ctype == "battery-level": + self._chars['battery-level'] = characteristic['iid'] + self._battery_level = characteristic['value'] + + @property + def name(self): + """Return the name of this device.""" + return self._name + + @property + def is_locked(self): + """Return true if device is locked.""" + return self._state == STATE_LOCKED + + @property + def available(self): + """Return True if entity is available.""" + return self._state is not None + + def lock(self, **kwargs): + """Lock the device.""" + self._set_lock_state(STATE_LOCKED) + + def unlock(self, **kwargs): + """Unlock the device.""" + self._set_lock_state(STATE_UNLOCKED) + + def _set_lock_state(self, state): + """Send state command.""" + characteristics = [{'aid': self._aid, + 'iid': self._chars['lock-mechanism.target-state'], + 'value': TARGET_STATE_MAP[state]}] + self.put_characteristics(characteristics) + + @property + def device_state_attributes(self): + """Return the optional state attributes.""" + if self._battery_level is None: + return None + + return { + ATTR_BATTERY_LEVEL: self._battery_level, + } From ba2160804222c8aa2426ab627150b1320b5590a0 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Fri, 11 Jan 2019 23:34:48 -0500 Subject: [PATCH 127/235] Repackage ZHA component (#19989) * move files * rename files * rename files * move files * relative import * update coveragerc --- .coveragerc | 5 +++++ .../{binary_sensor/zha.py => zha/binary_sensor.py} | 0 homeassistant/components/zha/const.py | 4 ++-- homeassistant/components/{fan/zha.py => zha/fan.py} | 0 homeassistant/components/{light/zha.py => zha/light.py} | 0 homeassistant/components/{sensor/zha.py => zha/sensor.py} | 0 homeassistant/components/{switch/zha.py => zha/switch.py} | 0 7 files changed, 7 insertions(+), 2 deletions(-) rename homeassistant/components/{binary_sensor/zha.py => zha/binary_sensor.py} (100%) rename homeassistant/components/{fan/zha.py => zha/fan.py} (100%) rename homeassistant/components/{light/zha.py => zha/light.py} (100%) rename homeassistant/components/{sensor/zha.py => zha/sensor.py} (100%) rename homeassistant/components/{switch/zha.py => zha/switch.py} (100%) diff --git a/.coveragerc b/.coveragerc index f062501bd35..568dfd79386 100644 --- a/.coveragerc +++ b/.coveragerc @@ -424,8 +424,13 @@ omit = homeassistant/components/*/zabbix.py homeassistant/components/zha/__init__.py + homeassistant/components/zha/binary_sensor.py homeassistant/components/zha/const.py homeassistant/components/zha/event.py + homeassistant/components/zha/fan.py + homeassistant/components/zha/light.py + homeassistant/components/zha/sensor.py + homeassistant/components/zha/switch.py homeassistant/components/zha/api.py homeassistant/components/zha/entities/* homeassistant/components/zha/helpers.py diff --git a/homeassistant/components/binary_sensor/zha.py b/homeassistant/components/zha/binary_sensor.py similarity index 100% rename from homeassistant/components/binary_sensor/zha.py rename to homeassistant/components/zha/binary_sensor.py diff --git a/homeassistant/components/zha/const.py b/homeassistant/components/zha/const.py index 29295e35060..ebcf020a9b8 100644 --- a/homeassistant/components/zha/const.py +++ b/homeassistant/components/zha/const.py @@ -102,7 +102,7 @@ def populate_data(): """ from zigpy import zcl, quirks from zigpy.profiles import PROFILES, zha, zll - from homeassistant.components.sensor import zha as sensor_zha + from .sensor import RelativeHumiditySensor if zha.PROFILE_ID not in DEVICE_CLASS: DEVICE_CLASS[zha.PROFILE_ID] = {} @@ -162,7 +162,7 @@ def populate_data(): # A map of device/cluster to component/sub-component CUSTOM_CLUSTER_MAPPINGS.update({ (quirks.smartthings.SmartthingsTemperatureHumiditySensor, 64581): - ('sensor', sensor_zha.RelativeHumiditySensor) + ('sensor', RelativeHumiditySensor) }) # A map of hass components to all Zigbee clusters it could use diff --git a/homeassistant/components/fan/zha.py b/homeassistant/components/zha/fan.py similarity index 100% rename from homeassistant/components/fan/zha.py rename to homeassistant/components/zha/fan.py diff --git a/homeassistant/components/light/zha.py b/homeassistant/components/zha/light.py similarity index 100% rename from homeassistant/components/light/zha.py rename to homeassistant/components/zha/light.py diff --git a/homeassistant/components/sensor/zha.py b/homeassistant/components/zha/sensor.py similarity index 100% rename from homeassistant/components/sensor/zha.py rename to homeassistant/components/zha/sensor.py diff --git a/homeassistant/components/switch/zha.py b/homeassistant/components/zha/switch.py similarity index 100% rename from homeassistant/components/switch/zha.py rename to homeassistant/components/zha/switch.py From 418fa226e60d2827756e95d3b9f8c92e8a4094b8 Mon Sep 17 00:00:00 2001 From: Sriram Vaidyanathan Date: Sat, 12 Jan 2019 10:14:01 +0530 Subject: [PATCH 128/235] 'latest_dir' referenced before assignment (#19952) * 'latest_dir' referenced before assignment local variable 'latest_dir' referenced before assignment * Better fix --- homeassistant/components/camera/xiaomi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/camera/xiaomi.py b/homeassistant/components/camera/xiaomi.py index 916a4cb9a90..207dd17ed9b 100644 --- a/homeassistant/components/camera/xiaomi.py +++ b/homeassistant/components/camera/xiaomi.py @@ -107,7 +107,7 @@ class XiaomiCamera(Camera): _LOGGER.warning("There don't appear to be any folders") return False - first_dir = dirs[-1] + first_dir = latest_dir = dirs[-1] try: ftp.cwd(first_dir) except error_perm as exc: From 2208563de44d6655e44f8608889f292ff4a565e3 Mon Sep 17 00:00:00 2001 From: Thomas Delaet Date: Sat, 12 Jan 2019 05:44:16 +0100 Subject: [PATCH 129/235] catch TypeError's in addition to ValueError's for unifi direct device tracker (#19994) * catch TypeError's in addition to ValueError's in response from unifi access point sometimes unifi's access point returns incomplete json which results in a TypeError because ssid_table is None * fix syntax error --- homeassistant/components/device_tracker/unifi_direct.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/device_tracker/unifi_direct.py b/homeassistant/components/device_tracker/unifi_direct.py index 3b5dcc8bac2..bd90099e45c 100644 --- a/homeassistant/components/device_tracker/unifi_direct.py +++ b/homeassistant/components/device_tracker/unifi_direct.py @@ -131,6 +131,6 @@ def _response_to_json(response): active_clients[client.get("mac")] = client return active_clients - except ValueError: + except (ValueError, TypeError): _LOGGER.error("Failed to decode response from AP.") return {} From 3bdee570660b8cebac897053dfe88d4668e47479 Mon Sep 17 00:00:00 2001 From: Tommy Jonsson Date: Sat, 12 Jan 2019 05:44:29 +0100 Subject: [PATCH 130/235] Support for html5 notifications to suggest their names (#19965) * support for devices to suggest their names * houndci fixes * Lint --- homeassistant/components/notify/html5.py | 12 ++++++-- tests/components/notify/test_html5.py | 38 ++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/notify/html5.py b/homeassistant/components/notify/html5.py index 771606b935f..f70a9cb73c1 100644 --- a/homeassistant/components/notify/html5.py +++ b/homeassistant/components/notify/html5.py @@ -44,6 +44,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ ATTR_SUBSCRIPTION = 'subscription' ATTR_BROWSER = 'browser' +ATTR_NAME = 'name' ATTR_ENDPOINT = 'endpoint' ATTR_KEYS = 'keys' @@ -82,6 +83,7 @@ SUBSCRIPTION_SCHEMA = vol.All( REGISTER_SCHEMA = vol.Schema({ vol.Required(ATTR_SUBSCRIPTION): SUBSCRIPTION_SCHEMA, vol.Required(ATTR_BROWSER): vol.In(['chrome', 'firefox']), + vol.Optional(ATTR_NAME): cv.string }) CALLBACK_EVENT_PAYLOAD_SCHEMA = vol.Schema({ @@ -156,7 +158,10 @@ class HTML5PushRegistrationView(HomeAssistantView): return self.json_message( humanize_error(data, ex), HTTP_BAD_REQUEST) - name = self.find_registration_name(data) + devname = data.get(ATTR_NAME) + data.pop(ATTR_NAME, None) + + name = self.find_registration_name(data, devname) previous_registration = self.registrations.get(name) self.registrations[name] = data @@ -177,14 +182,15 @@ class HTML5PushRegistrationView(HomeAssistantView): return self.json_message( 'Error saving registration.', HTTP_INTERNAL_SERVER_ERROR) - def find_registration_name(self, data): + def find_registration_name(self, data, suggested=None): """Find a registration name matching data or generate a unique one.""" endpoint = data.get(ATTR_SUBSCRIPTION).get(ATTR_ENDPOINT) for key, registration in self.registrations.items(): subscription = registration.get(ATTR_SUBSCRIPTION) if subscription.get(ATTR_ENDPOINT) == endpoint: return key - return ensure_unique_string('unnamed device', self.registrations) + return ensure_unique_string(suggested or 'unnamed device', + self.registrations) async def delete(self, request): """Delete a registration.""" diff --git a/tests/components/notify/test_html5.py b/tests/components/notify/test_html5.py index 08210ecd9a2..6aeba650a8c 100644 --- a/tests/components/notify/test_html5.py +++ b/tests/components/notify/test_html5.py @@ -165,6 +165,23 @@ async def test_registering_new_device_view(hass, hass_client): } +async def test_registering_new_device_view_with_name(hass, hass_client): + """Test that the HTML view works with name attribute.""" + client = await mock_client(hass, hass_client) + + SUB_WITH_NAME = SUBSCRIPTION_1.copy() + SUB_WITH_NAME['name'] = 'test device' + + with patch('homeassistant.components.notify.html5.save_json') as mock_save: + resp = await client.post(REGISTER_URL, data=json.dumps(SUB_WITH_NAME)) + + assert resp.status == 200 + assert len(mock_save.mock_calls) == 1 + assert mock_save.mock_calls[0][1][1] == { + 'test device': SUBSCRIPTION_1, + } + + async def test_registering_new_device_expiration_view(hass, hass_client): """Test that the HTML view works.""" client = await mock_client(hass, hass_client) @@ -209,6 +226,27 @@ async def test_registering_existing_device_view(hass, hass_client): } +async def test_registering_existing_device_view_with_name(hass, hass_client): + """Test subscription is updated when reg'ing existing device with name.""" + registrations = {} + client = await mock_client(hass, hass_client, registrations) + + SUB_WITH_NAME = SUBSCRIPTION_1.copy() + SUB_WITH_NAME['name'] = 'test device' + + with patch('homeassistant.components.notify.html5.save_json') as mock_save: + await client.post(REGISTER_URL, data=json.dumps(SUB_WITH_NAME)) + resp = await client.post(REGISTER_URL, data=json.dumps(SUBSCRIPTION_4)) + + assert resp.status == 200 + assert mock_save.mock_calls[0][1][1] == { + 'test device': SUBSCRIPTION_4, + } + assert registrations == { + 'test device': SUBSCRIPTION_4, + } + + async def test_registering_existing_device_fails_view(hass, hass_client): """Test sub. is not updated when registering existing device fails.""" registrations = {} From b3580f46b955934659757a8330ba000d98f9a979 Mon Sep 17 00:00:00 2001 From: Matt Snyder Date: Fri, 11 Jan 2019 22:45:03 -0600 Subject: [PATCH 131/235] Update doorbird events to include URLs on event_data (#19262) * Update doorbird events to include URLs on event_data as shown in documentation. (cherry picked from commit 2405bc96fe1b3a93ea8466e48db6b7df8df02235) * Format timestamp * Update timestamp * Lint --- homeassistant/components/doorbird.py | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/doorbird.py b/homeassistant/components/doorbird.py index a3aab7e8dd9..f952d1ddc1c 100644 --- a/homeassistant/components/doorbird.py +++ b/homeassistant/components/doorbird.py @@ -12,7 +12,7 @@ from homeassistant.components.http import HomeAssistantView from homeassistant.const import CONF_HOST, CONF_USERNAME, \ CONF_PASSWORD, CONF_NAME, CONF_DEVICES, CONF_MONITORED_CONDITIONS import homeassistant.helpers.config_validation as cv -from homeassistant.util import slugify +from homeassistant.util import slugify, dt as dt_util REQUIREMENTS = ['doorbirdpy==2.0.4'] @@ -319,6 +319,16 @@ class ConfiguredDoorBird(): return None + def get_event_data(self): + """Get data to pass along with HA event.""" + return { + 'timestamp': dt_util.utcnow().isoformat(), + 'live_video_url': self._device.live_video_url, + 'live_image_url': self._device.live_image_url, + 'rtsp_live_video_url': self._device.rtsp_live_video_url, + 'html5_viewer_url': self._device.html5_viewer_url + } + class DoorBirdRequestView(HomeAssistantView): """Provide a page for the device to call.""" @@ -346,7 +356,14 @@ class DoorBirdRequestView(HomeAssistantView): if request_token == '' or not authenticated: return web.Response(status=401, text='Unauthorized') - hass.bus.async_fire('{}_{}'.format(DOMAIN, sensor)) + doorstation = get_doorstation_by_slug(hass, sensor) + + if doorstation: + event_data = doorstation.get_event_data() + else: + event_data = {} + + hass.bus.async_fire('{}_{}'.format(DOMAIN, sensor), event_data) return web.Response(status=200, text='OK') From 6395087a406c9d56af55b4029462f3eb309ff350 Mon Sep 17 00:00:00 2001 From: Jonathan Keljo Date: Fri, 11 Jan 2019 20:54:22 -0800 Subject: [PATCH 132/235] Upgrade greeneye_monitor to 1.0 (#19631) * Upgrade greeneye_monitor to 1.0 This is a breaking change; it causes the `serial_number` field in configuration to be treated as the full 8-digit serial number rather than the last 5 digits as was previously done, which results in the unique identifiers for the sensors being different. (Fixing them up in `config/.storage/core.entity_registry` before rebooting into the updated version seems to prevent any weirdness.) The last-5-digits behavior was a result of me misunderstanding the packet format docs and not realizing that the true serial number was split across two fields. In addition to being confusing (see https://community.home-assistant.io/t/brultech-greeneye-issues/86852), it was technically incorrect. The `greeneye_monitor` platform was just introduced in 0.82, so it seems like the kind of thing that's best to fix now while adoption is relatively low rather than later when somebody runs into it as more than just a point of confusion. * Switch to 8-character string * Coerce to int * Remove now-unnecessary cast --- homeassistant/components/greeneye_monitor.py | 12 ++++++++++-- requirements_all.txt | 2 +- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/greeneye_monitor.py b/homeassistant/components/greeneye_monitor.py index f5c51da88be..c1e2f285772 100644 --- a/homeassistant/components/greeneye_monitor.py +++ b/homeassistant/components/greeneye_monitor.py @@ -16,7 +16,7 @@ from homeassistant.const import ( import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import async_load_platform -REQUIREMENTS = ['greeneye_monitor==0.1'] +REQUIREMENTS = ['greeneye_monitor==1.0'] _LOGGER = logging.getLogger(__name__) @@ -81,7 +81,15 @@ CHANNEL_SCHEMA = vol.Schema({ CHANNELS_SCHEMA = vol.All(cv.ensure_list, [CHANNEL_SCHEMA]) MONITOR_SCHEMA = vol.Schema({ - vol.Required(CONF_SERIAL_NUMBER): cv.positive_int, + vol.Required(CONF_SERIAL_NUMBER): + vol.All( + cv.string, + vol.Length( + min=8, + max=8, + msg="GEM serial number must be specified as an 8-character " + "string (including leading zeroes)."), + vol.Coerce(int)), vol.Optional(CONF_CHANNELS, default=[]): CHANNELS_SCHEMA, vol.Optional( CONF_TEMPERATURE_SENSORS, diff --git a/requirements_all.txt b/requirements_all.txt index 51cbc19a7e4..947eca9876b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -466,7 +466,7 @@ googlemaps==2.5.1 gps3==0.33.3 # homeassistant.components.greeneye_monitor -greeneye_monitor==0.1 +greeneye_monitor==1.0 # homeassistant.components.light.greenwave greenwavereality==0.5.1 From 012e91f9b176bfce9fa69561f871b37676c62cf0 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Sat, 12 Jan 2019 09:21:30 -0500 Subject: [PATCH 133/235] version bump for zha-quirks (#20019) --- homeassistant/components/zha/__init__.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index 26d11586a1b..d47e42511f5 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -37,7 +37,7 @@ REQUIREMENTS = [ 'bellows==0.7.0', 'zigpy==0.2.0', 'zigpy-xbee==0.1.1', - 'zha-quirks==0.0.5' + 'zha-quirks==0.0.6' ] DEVICE_CONFIG_SCHEMA_ENTRY = vol.Schema({ diff --git a/requirements_all.txt b/requirements_all.txt index 947eca9876b..ba348a228ec 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1735,7 +1735,7 @@ zengge==0.2 zeroconf==0.21.3 # homeassistant.components.zha -zha-quirks==0.0.5 +zha-quirks==0.0.6 # homeassistant.components.climate.zhong_hong zhong_hong_hvac==1.0.9 From eabc7b22cd9ec3b38cb6a90160d1cae18d89a80c Mon Sep 17 00:00:00 2001 From: carstenschroeder Date: Sat, 12 Jan 2019 16:36:50 +0100 Subject: [PATCH 134/235] Enable bool type for ADS service (#20011) --- homeassistant/components/ads/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/ads/__init__.py b/homeassistant/components/ads/__init__.py index 015a12dcfa9..657ff8acc3a 100644 --- a/homeassistant/components/ads/__init__.py +++ b/homeassistant/components/ads/__init__.py @@ -46,7 +46,7 @@ CONFIG_SCHEMA = vol.Schema({ SCHEMA_SERVICE_WRITE_DATA_BY_NAME = vol.Schema({ vol.Required(CONF_ADS_TYPE): - vol.In([ADSTYPE_INT, ADSTYPE_UINT, ADSTYPE_BYTE]), + vol.In([ADSTYPE_INT, ADSTYPE_UINT, ADSTYPE_BYTE, ADSTYPE_BOOL]), vol.Required(CONF_ADS_VALUE): cv.match_all, vol.Required(CONF_ADS_VAR): cv.string, }) From d3f2854c898ef36650f06ffd7a8cd7d332d57876 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sat, 12 Jan 2019 17:59:44 +0100 Subject: [PATCH 135/235] UniFi - Fix issue with POE switch reset switch config (#20021) * Fix issue when controlling POE would reset configuration for all other ports on same device --- homeassistant/components/unifi/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/switch/test_unifi.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/unifi/__init__.py b/homeassistant/components/unifi/__init__.py index 1e07d8cb83d..8476aade7d7 100644 --- a/homeassistant/components/unifi/__init__.py +++ b/homeassistant/components/unifi/__init__.py @@ -22,7 +22,7 @@ DEFAULT_PORT = 8443 DEFAULT_SITE_ID = 'default' DEFAULT_VERIFY_SSL = False -REQUIREMENTS = ['aiounifi==3'] +REQUIREMENTS = ['aiounifi==4'] async def async_setup(hass, config): diff --git a/requirements_all.txt b/requirements_all.txt index ba348a228ec..2afde16d51e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -127,7 +127,7 @@ aiolifx_effects==0.2.1 aiopvapi==1.6.14 # homeassistant.components.unifi -aiounifi==3 +aiounifi==4 # homeassistant.components.cover.aladdin_connect aladdin_connect==0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5f761333e31..5554af95123 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -41,7 +41,7 @@ aiohttp_cors==0.7.0 aiohue==1.5.0 # homeassistant.components.unifi -aiounifi==3 +aiounifi==4 # homeassistant.components.notify.apns apns2==0.3.0 diff --git a/tests/components/switch/test_unifi.py b/tests/components/switch/test_unifi.py index 0513b5724db..67f1e416cf1 100644 --- a/tests/components/switch/test_unifi.py +++ b/tests/components/switch/test_unifi.py @@ -109,7 +109,7 @@ DEVICE_1 = { 'mac': '00:00:00:00:01:01', 'type': 'usw', 'name': 'mock-name', - 'portconf_id': '', + 'port_overrides': [], 'port_table': [ { 'media': 'GE', From 22c0733d8ea56b460b8597380ead1de70c93ba02 Mon Sep 17 00:00:00 2001 From: ehendrix23 Date: Sat, 12 Jan 2019 11:02:00 -0700 Subject: [PATCH 136/235] Add service change_channel to Harmony component (#19649) * Update requirements Updated requirements * Add attributes Add firmware and config version attributes * Small bump for aioharmony Small version bump increase for aioharmony * Order ATTR constants * Add the service Add service change_channel * Fix requirements file For some reason aioharmony ended up in there as a duplicate. Fixed it. * Updates based on review --- homeassistant/components/remote/harmony.py | 29 ++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/homeassistant/components/remote/harmony.py b/homeassistant/components/remote/harmony.py index 4df471aa918..07fd5831dbb 100644 --- a/homeassistant/components/remote/harmony.py +++ b/homeassistant/components/remote/harmony.py @@ -26,6 +26,7 @@ REQUIREMENTS = ['aioharmony==0.1.2'] _LOGGER = logging.getLogger(__name__) +ATTR_CHANNEL = 'channel' ATTR_CURRENT_ACTIVITY = 'current_activity' DEFAULT_PORT = 8088 @@ -33,6 +34,7 @@ DEVICES = [] CONF_DEVICE_CACHE = 'harmony_device_cache' SERVICE_SYNC = 'harmony_sync' +SERVICE_CHANGE_CHANNEL = 'harmony_change_channel' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(ATTR_ACTIVITY): cv.string, @@ -47,6 +49,11 @@ HARMONY_SYNC_SCHEMA = vol.Schema({ vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, }) +HARMONY_CHANGE_CHANNEL_SCHEMA = vol.Schema({ + vol.Required(ATTR_ENTITY_ID): cv.entity_ids, + vol.Required(ATTR_CHANNEL): cv.positive_int +}) + async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): @@ -112,6 +119,10 @@ def register_services(hass): DOMAIN, SERVICE_SYNC, _sync_service, schema=HARMONY_SYNC_SCHEMA) + hass.services.async_register( + DOMAIN, SERVICE_CHANGE_CHANNEL, _change_channel_service, + schema=HARMONY_CHANGE_CHANNEL_SCHEMA) + async def _apply_service(service, service_func, *service_func_args): """Handle services to apply.""" @@ -131,6 +142,11 @@ async def _sync_service(service): await _apply_service(service, HarmonyRemote.sync) +async def _change_channel_service(service): + channel = service.data.get(ATTR_CHANNEL) + await _apply_service(service, HarmonyRemote.change_channel, channel) + + class HarmonyRemote(remote.RemoteDevice): """Remote representation used to control a Harmony device.""" @@ -348,6 +364,19 @@ class HarmonyRemote(remote.RemoteDevice): result.command.msg ) + async def change_channel(self, channel): + """Change the channel using Harmony remote.""" + import aioharmony.exceptions as aioexc + + _LOGGER.debug("%s: Changing channel to %s", + self.name, channel) + try: + await self._client.change_channel(channel) + except aioexc.TimeOut: + _LOGGER.error("%s: Changing channel to %s timed-out", + self.name, + channel) + async def sync(self): """Sync the Harmony device with the web service.""" import aioharmony.exceptions as aioexc From 218c82eaf3223f3c7ec8481b62e0755dbe71b692 Mon Sep 17 00:00:00 2001 From: Sean Dague Date: Sat, 12 Jan 2019 13:51:01 -0500 Subject: [PATCH 137/235] mychevy: Fix wrong attribute on battery level selector (#20016) The battery level sensor is broken because the logic for determining if the battery is charged accessed the wrong variable. This one character fix makes it work again. --- homeassistant/components/sensor/mychevy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/mychevy.py b/homeassistant/components/sensor/mychevy.py index a835321991d..b478e2ef3ca 100644 --- a/homeassistant/components/sensor/mychevy.py +++ b/homeassistant/components/sensor/mychevy.py @@ -141,7 +141,7 @@ class EVSensor(Entity): def icon(self): """Return the icon.""" if self._attr == BATTERY_SENSOR: - charging = self.state_attributes.get("charging", False) + charging = self._state_attributes.get("charging", False) return icon_for_battery_level(self.state, charging) return self._icon From 04636e9ba7196d4539cc369e39b8f349bfb7062d Mon Sep 17 00:00:00 2001 From: ehendrix23 Date: Sat, 12 Jan 2019 12:15:27 -0700 Subject: [PATCH 138/235] Add harmony service to remote services.yaml (#20031) Added the harmony_change_channel service to services.yaml for remote component. --- homeassistant/components/remote/services.yaml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/homeassistant/components/remote/services.yaml b/homeassistant/components/remote/services.yaml index 25ad626f96d..ea3a48e253a 100644 --- a/homeassistant/components/remote/services.yaml +++ b/homeassistant/components/remote/services.yaml @@ -50,6 +50,16 @@ harmony_sync: description: Name(s) of entities to sync. example: 'remote.family_room' +harmony_change_channel: + description: Sends change channel command to the Harmony HUB + fields: + entity_id: + description: Name(s) of Harmony remote entities to send change channel command to + example: 'remote.family_room' + channel: + description: Channel number to change to + example: '200' + xiaomi_miio_learn_command: description: 'Learn an IR command, press "Call Service", point the remote at the IR device, and the learned command will be shown as a notification in Overview.' fields: From 4e020b90e113135225c86ce0ddd8712a21a0f145 Mon Sep 17 00:00:00 2001 From: Rohan Kapoor Date: Sat, 12 Jan 2019 19:18:33 -0800 Subject: [PATCH 139/235] Switch locative to use the webhook component --- .../components/locative/.translations/en.json | 18 +++ homeassistant/components/locative/__init__.py | 144 +++++++++--------- .../components/locative/strings.json | 18 +++ homeassistant/config_entries.py | 1 + tests/components/locative/test_init.py | 71 ++++++--- 5 files changed, 158 insertions(+), 94 deletions(-) create mode 100644 homeassistant/components/locative/.translations/en.json create mode 100644 homeassistant/components/locative/strings.json diff --git a/homeassistant/components/locative/.translations/en.json b/homeassistant/components/locative/.translations/en.json new file mode 100644 index 00000000000..b2a538a0fa5 --- /dev/null +++ b/homeassistant/components/locative/.translations/en.json @@ -0,0 +1,18 @@ +{ + "config": { + "title": "Locative Webhook", + "step": { + "user": { + "title": "Set up the Locative Webhook", + "description": "Are you sure you want to set up the Locative Webhook?" + } + }, + "abort": { + "one_instance_allowed": "Only a single instance is necessary.", + "not_internet_accessible": "Your Home Assistant instance needs to be accessible from the internet to receive messages from Geofency." + }, + "create_entry": { + "default": "To send locations to Home Assistant, you will need to setup the webhook feature in the Locative app.\n\nFill in the following info:\n\n- URL: `{webhook_url}`\n- Method: POST\n\nSee [the documentation]({docs_url}) for further details." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/locative/__init__.py b/homeassistant/components/locative/__init__.py index 1f924987dd3..7a4cfcc991b 100644 --- a/homeassistant/components/locative/__init__.py +++ b/homeassistant/components/locative/__init__.py @@ -8,76 +8,72 @@ import logging from homeassistant.components.device_tracker import \ DOMAIN as DEVICE_TRACKER_DOMAIN -from homeassistant.components.http import HomeAssistantView from homeassistant.const import HTTP_UNPROCESSABLE_ENTITY, ATTR_LATITUDE, \ - ATTR_LONGITUDE, STATE_NOT_HOME + ATTR_LONGITUDE, STATE_NOT_HOME, CONF_WEBHOOK_ID +from homeassistant.helpers import config_entry_flow from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.dispatcher import async_dispatcher_send _LOGGER = logging.getLogger(__name__) DOMAIN = 'locative' -DEPENDENCIES = ['http'] - -URL = '/api/locative' +DEPENDENCIES = ['webhook'] TRACKER_UPDATE = '{}_tracker_update'.format(DOMAIN) async def async_setup(hass, hass_config): """Set up the Locative component.""" - hass.http.register_view(LocativeView) hass.async_create_task( async_load_platform(hass, 'device_tracker', DOMAIN, {}, hass_config) ) return True -class LocativeView(HomeAssistantView): - """View to handle Locative requests.""" +async def handle_webhook(hass, webhook_id, request): + """Handle incoming webhook from Locative.""" + data = await request.post() - url = URL - name = 'api:locative' + if 'latitude' not in data or 'longitude' not in data: + return ('Latitude and longitude not specified.', + HTTP_UNPROCESSABLE_ENTITY) - def __init__(self): - """Initialize Locative URL endpoints.""" + if 'device' not in data: + _LOGGER.error('Device id not specified.') + return ('Device id not specified.', + HTTP_UNPROCESSABLE_ENTITY) - async def get(self, request): - """Locative message received as GET.""" - return await self._handle(request.app['hass'], request.query) + if 'trigger' not in data: + _LOGGER.error('Trigger is not specified.') + return ('Trigger is not specified.', + HTTP_UNPROCESSABLE_ENTITY) - async def post(self, request): - """Locative message received.""" - data = await request.post() - return await self._handle(request.app['hass'], data) + if 'id' not in data and data['trigger'] != 'test': + _LOGGER.error('Location id not specified.') + return ('Location id not specified.', + HTTP_UNPROCESSABLE_ENTITY) - async def _handle(self, hass, data): - """Handle locative request.""" - if 'latitude' not in data or 'longitude' not in data: - return ('Latitude and longitude not specified.', - HTTP_UNPROCESSABLE_ENTITY) + device = data['device'].replace('-', '') + location_name = data.get('id', data['trigger']).lower() + direction = data['trigger'] + gps_location = (data[ATTR_LATITUDE], data[ATTR_LONGITUDE]) - if 'device' not in data: - _LOGGER.error('Device id not specified.') - return ('Device id not specified.', - HTTP_UNPROCESSABLE_ENTITY) + if direction == 'enter': + async_dispatcher_send( + hass, + TRACKER_UPDATE, + device, + gps_location, + location_name + ) + return 'Setting location to {}'.format(location_name) - if 'trigger' not in data: - _LOGGER.error('Trigger is not specified.') - return ('Trigger is not specified.', - HTTP_UNPROCESSABLE_ENTITY) + if direction == 'exit': + current_state = hass.states.get( + '{}.{}'.format(DEVICE_TRACKER_DOMAIN, device)) - if 'id' not in data and data['trigger'] != 'test': - _LOGGER.error('Location id not specified.') - return ('Location id not specified.', - HTTP_UNPROCESSABLE_ENTITY) - - device = data['device'].replace('-', '') - location_name = data.get('id', data['trigger']).lower() - direction = data['trigger'] - gps_location = (data[ATTR_LATITUDE], data[ATTR_LONGITUDE]) - - if direction == 'enter': + if current_state is None or current_state.state == location_name: + location_name = STATE_NOT_HOME async_dispatcher_send( hass, TRACKER_UPDATE, @@ -85,36 +81,42 @@ class LocativeView(HomeAssistantView): gps_location, location_name ) - return 'Setting location to {}'.format(location_name) + return 'Setting location to not home' - if direction == 'exit': - current_state = hass.states.get( - '{}.{}'.format(DEVICE_TRACKER_DOMAIN, device)) + # Ignore the message if it is telling us to exit a zone that we + # aren't currently in. This occurs when a zone is entered + # before the previous zone was exited. The enter message will + # be sent first, then the exit message will be sent second. + return 'Ignoring exit from {} (already in {})'.format( + location_name, current_state) - if current_state is None or current_state.state == location_name: - location_name = STATE_NOT_HOME - async_dispatcher_send( - hass, - TRACKER_UPDATE, - device, - gps_location, - location_name - ) - return 'Setting location to not home' + if direction == 'test': + # In the app, a test message can be sent. Just return something to + # the user to let them know that it works. + return 'Received test message.' - # Ignore the message if it is telling us to exit a zone that we - # aren't currently in. This occurs when a zone is entered - # before the previous zone was exited. The enter message will - # be sent first, then the exit message will be sent second. - return 'Ignoring exit from {} (already in {})'.format( - location_name, current_state) + _LOGGER.error('Received unidentified message from Locative: %s', + direction) + return ('Received unidentified message: {}'.format(direction), + HTTP_UNPROCESSABLE_ENTITY) - if direction == 'test': - # In the app, a test message can be sent. Just return something to - # the user to let them know that it works. - return 'Received test message.' - _LOGGER.error('Received unidentified message from Locative: %s', - direction) - return ('Received unidentified message: {}'.format(direction), - HTTP_UNPROCESSABLE_ENTITY) +async def async_setup_entry(hass, entry): + """Configure based on config entry.""" + hass.components.webhook.async_register( + DOMAIN, 'Locative', entry.data[CONF_WEBHOOK_ID], handle_webhook) + return True + + +async def async_unload_entry(hass, entry): + """Unload a config entry.""" + hass.components.webhook.async_unregister(entry.data[CONF_WEBHOOK_ID]) + return True + +config_entry_flow.register_webhook_flow( + DOMAIN, + 'Locative Webhook', + { + 'docs_url': 'https://www.home-assistant.io/components/locative/' + } +) \ No newline at end of file diff --git a/homeassistant/components/locative/strings.json b/homeassistant/components/locative/strings.json new file mode 100644 index 00000000000..b2a538a0fa5 --- /dev/null +++ b/homeassistant/components/locative/strings.json @@ -0,0 +1,18 @@ +{ + "config": { + "title": "Locative Webhook", + "step": { + "user": { + "title": "Set up the Locative Webhook", + "description": "Are you sure you want to set up the Locative Webhook?" + } + }, + "abort": { + "one_instance_allowed": "Only a single instance is necessary.", + "not_internet_accessible": "Your Home Assistant instance needs to be accessible from the internet to receive messages from Geofency." + }, + "create_entry": { + "default": "To send locations to Home Assistant, you will need to setup the webhook feature in the Locative app.\n\nFill in the following info:\n\n- URL: `{webhook_url}`\n- Method: POST\n\nSee [the documentation]({docs_url}) for further details." + } + } +} \ No newline at end of file diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index dde542e5fc0..fbc44b3775e 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -148,6 +148,7 @@ FLOWS = [ 'ifttt', 'ios', 'lifx', + 'locative', 'luftdaten', 'mailgun', 'mqtt', diff --git a/tests/components/locative/test_init.py b/tests/components/locative/test_init.py index 50c72f468af..067bb73236b 100644 --- a/tests/components/locative/test_init.py +++ b/tests/components/locative/test_init.py @@ -1,11 +1,12 @@ """The tests the for Locative device tracker platform.""" -from unittest.mock import patch +from unittest.mock import patch, Mock import pytest +from homeassistant import data_entry_flow from homeassistant.components.device_tracker import \ DOMAIN as DEVICE_TRACKER_DOMAIN -from homeassistant.components.locative import URL, DOMAIN +from homeassistant.components.locative import DOMAIN from homeassistant.const import HTTP_OK, HTTP_UNPROCESSABLE_ENTITY from homeassistant.setup import async_setup_component @@ -36,8 +37,26 @@ def locative_client(loop, hass, hass_client): yield loop.run_until_complete(hass_client()) -async def test_missing_data(locative_client): +@pytest.fixture +async def webhook_id(hass, locative_client): + """Initialize the Geofency component and get the webhook_id.""" + hass.config.api = Mock(base_url='http://example.com') + result = await hass.config_entries.flow.async_init('locative', context={ + 'source': 'user' + }) + assert result['type'] == data_entry_flow.RESULT_TYPE_FORM, result + + result = await hass.config_entries.flow.async_configure( + result['flow_id'], {}) + assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + + return result['result'].data['webhook_id'] + + +async def test_missing_data(locative_client, webhook_id): """Test missing data.""" + url = '/api/webhook/{}'.format(webhook_id) + data = { 'latitude': 1.0, 'longitude': 1.1, @@ -47,55 +66,57 @@ async def test_missing_data(locative_client): } # No data - req = await locative_client.get(_url({})) + req = await locative_client.post(url) assert req.status == HTTP_UNPROCESSABLE_ENTITY # No latitude copy = data.copy() del copy['latitude'] - req = await locative_client.get(_url(copy)) + req = await locative_client.post(url, data=copy) assert req.status == HTTP_UNPROCESSABLE_ENTITY # No device copy = data.copy() del copy['device'] - req = await locative_client.get(_url(copy)) + req = await locative_client.post(url, data=copy) assert req.status == HTTP_UNPROCESSABLE_ENTITY # No location copy = data.copy() del copy['id'] - req = await locative_client.get(_url(copy)) + req = await locative_client.post(url, data=copy) assert req.status == HTTP_UNPROCESSABLE_ENTITY # No trigger copy = data.copy() del copy['trigger'] - req = await locative_client.get(_url(copy)) + req = await locative_client.post(url, data=copy) assert req.status == HTTP_UNPROCESSABLE_ENTITY # Test message copy = data.copy() copy['trigger'] = 'test' - req = await locative_client.get(_url(copy)) + req = await locative_client.post(url, data=copy) assert req.status == HTTP_OK # Test message, no location copy = data.copy() copy['trigger'] = 'test' del copy['id'] - req = await locative_client.get(_url(copy)) + req = await locative_client.post(url, data=copy) assert req.status == HTTP_OK # Unknown trigger copy = data.copy() copy['trigger'] = 'foobar' - req = await locative_client.get(_url(copy)) + req = await locative_client.post(url, data=copy) assert req.status == HTTP_UNPROCESSABLE_ENTITY -async def test_enter_and_exit(hass, locative_client): +async def test_enter_and_exit(hass, locative_client, webhook_id): """Test when there is a known zone.""" + url = '/api/webhook/{}'.format(webhook_id) + data = { 'latitude': 40.7855, 'longitude': -111.7367, @@ -105,7 +126,7 @@ async def test_enter_and_exit(hass, locative_client): } # Enter the Home - req = await locative_client.get(_url(data)) + req = await locative_client.post(url, data=data) await hass.async_block_till_done() assert req.status == HTTP_OK state_name = hass.states.get('{}.{}'.format(DEVICE_TRACKER_DOMAIN, @@ -116,7 +137,7 @@ async def test_enter_and_exit(hass, locative_client): data['trigger'] = 'exit' # Exit Home - req = await locative_client.get(_url(data)) + req = await locative_client.post(url, data=data) await hass.async_block_till_done() assert req.status == HTTP_OK state_name = hass.states.get('{}.{}'.format(DEVICE_TRACKER_DOMAIN, @@ -127,7 +148,7 @@ async def test_enter_and_exit(hass, locative_client): data['trigger'] = 'enter' # Enter Home again - req = await locative_client.get(_url(data)) + req = await locative_client.post(url, data=data) await hass.async_block_till_done() assert req.status == HTTP_OK state_name = hass.states.get('{}.{}'.format(DEVICE_TRACKER_DOMAIN, @@ -137,7 +158,7 @@ async def test_enter_and_exit(hass, locative_client): data['trigger'] = 'exit' # Exit Home - req = await locative_client.get(_url(data)) + req = await locative_client.post(url, data=data) await hass.async_block_till_done() assert req.status == HTTP_OK state_name = hass.states.get('{}.{}'.format(DEVICE_TRACKER_DOMAIN, @@ -148,7 +169,7 @@ async def test_enter_and_exit(hass, locative_client): data['trigger'] = 'enter' # Enter Work - req = await locative_client.get(_url(data)) + req = await locative_client.post(url, data=data) await hass.async_block_till_done() assert req.status == HTTP_OK state_name = hass.states.get('{}.{}'.format(DEVICE_TRACKER_DOMAIN, @@ -156,8 +177,10 @@ async def test_enter_and_exit(hass, locative_client): assert 'work' == state_name -async def test_exit_after_enter(hass, locative_client): +async def test_exit_after_enter(hass, locative_client, webhook_id): """Test when an exit message comes after an enter message.""" + url = '/api/webhook/{}'.format(webhook_id) + data = { 'latitude': 40.7855, 'longitude': -111.7367, @@ -167,7 +190,7 @@ async def test_exit_after_enter(hass, locative_client): } # Enter Home - req = await locative_client.get(_url(data)) + req = await locative_client.post(url, data=data) await hass.async_block_till_done() assert req.status == HTTP_OK @@ -178,7 +201,7 @@ async def test_exit_after_enter(hass, locative_client): data['id'] = 'Work' # Enter Work - req = await locative_client.get(_url(data)) + req = await locative_client.post(url, data=data) await hass.async_block_till_done() assert req.status == HTTP_OK @@ -190,7 +213,7 @@ async def test_exit_after_enter(hass, locative_client): data['trigger'] = 'exit' # Exit Home - req = await locative_client.get(_url(data)) + req = await locative_client.post(url, data=data) await hass.async_block_till_done() assert req.status == HTTP_OK @@ -199,8 +222,10 @@ async def test_exit_after_enter(hass, locative_client): assert state.state == 'work' -async def test_exit_first(hass, locative_client): +async def test_exit_first(hass, locative_client, webhook_id): """Test when an exit message is sent first on a new device.""" + url = '/api/webhook/{}'.format(webhook_id) + data = { 'latitude': 40.7855, 'longitude': -111.7367, @@ -210,7 +235,7 @@ async def test_exit_first(hass, locative_client): } # Exit Home - req = await locative_client.get(_url(data)) + req = await locative_client.post(url, data=data) await hass.async_block_till_done() assert req.status == HTTP_OK From 0007f35f967424d5866ef478293ad39d21074a24 Mon Sep 17 00:00:00 2001 From: Rohan Kapoor Date: Sat, 12 Jan 2019 19:23:19 -0800 Subject: [PATCH 140/235] Lint --- homeassistant/components/locative/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/locative/__init__.py b/homeassistant/components/locative/__init__.py index 7a4cfcc991b..f33974e2827 100644 --- a/homeassistant/components/locative/__init__.py +++ b/homeassistant/components/locative/__init__.py @@ -119,4 +119,4 @@ config_entry_flow.register_webhook_flow( { 'docs_url': 'https://www.home-assistant.io/components/locative/' } -) \ No newline at end of file +) From 3e325a4ef94c54508830d9e6a1e702cd1880dc44 Mon Sep 17 00:00:00 2001 From: Rohan Kapoor Date: Sat, 12 Jan 2019 20:24:55 -0800 Subject: [PATCH 141/235] Remove dead test code --- tests/components/locative/test_init.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/tests/components/locative/test_init.py b/tests/components/locative/test_init.py index 067bb73236b..5f18d47eb22 100644 --- a/tests/components/locative/test_init.py +++ b/tests/components/locative/test_init.py @@ -11,14 +11,6 @@ from homeassistant.const import HTTP_OK, HTTP_UNPROCESSABLE_ENTITY from homeassistant.setup import async_setup_component -def _url(data=None): - """Generate URL.""" - data = data or {} - data = "&".join(["{}={}".format(name, value) for - name, value in data.items()]) - return "{}?{}".format(URL, data) - - @pytest.fixture(autouse=True) def mock_dev_track(mock_device_tracker_conf): """Mock device tracker config loading.""" From e295ca7b8eb41f454429d53fec8e121a717e1f7d Mon Sep 17 00:00:00 2001 From: Caleb Dunn Date: Sun, 13 Jan 2019 01:56:05 -0600 Subject: [PATCH 142/235] update to pyunifi 2.16 (#20042) * update to pyunifi 2.16 * update requirements to version 2.16 --- homeassistant/components/device_tracker/unifi.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/device_tracker/unifi.py b/homeassistant/components/device_tracker/unifi.py index 627d2092a11..0c0c908d1e0 100644 --- a/homeassistant/components/device_tracker/unifi.py +++ b/homeassistant/components/device_tracker/unifi.py @@ -15,7 +15,7 @@ from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD from homeassistant.const import CONF_VERIFY_SSL, CONF_MONITORED_CONDITIONS import homeassistant.util.dt as dt_util -REQUIREMENTS = ['pyunifi==2.13'] +REQUIREMENTS = ['pyunifi==2.16'] _LOGGER = logging.getLogger(__name__) CONF_PORT = 'port' diff --git a/requirements_all.txt b/requirements_all.txt index 2afde16d51e..a99eadc4473 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1365,7 +1365,7 @@ pytradfri[async]==6.0.1 pytrafikverket==0.1.5.8 # homeassistant.components.device_tracker.unifi -pyunifi==2.13 +pyunifi==2.16 # homeassistant.components.binary_sensor.uptimerobot pyuptimerobot==0.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5554af95123..1b72da7c89c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -224,7 +224,7 @@ pythonwhois==2.4.3 pytradfri[async]==6.0.1 # homeassistant.components.device_tracker.unifi -pyunifi==2.13 +pyunifi==2.16 # homeassistant.components.notify.html5 pywebpush==1.6.0 From 162e2b83857ecc5efe8e633bcfa9d3507429c63e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sun, 13 Jan 2019 09:56:26 +0200 Subject: [PATCH 143/235] Upgrade pytest to 4.1.0 (#20013) --- 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 e8775ea57c8..a2a52524210 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -13,5 +13,5 @@ pytest-aiohttp==0.3.0 pytest-cov==2.6.1 pytest-sugar==0.9.2 pytest-timeout==1.3.3 -pytest==4.0.2 +pytest==4.1.0 requests_mock==1.5.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1b72da7c89c..37670a72909 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -14,7 +14,7 @@ pytest-aiohttp==0.3.0 pytest-cov==2.6.1 pytest-sugar==0.9.2 pytest-timeout==1.3.3 -pytest==4.0.2 +pytest==4.1.0 requests_mock==1.5.2 From 1c11394f5fe38678e2c7515c00cb2892043dd780 Mon Sep 17 00:00:00 2001 From: Alok Saboo Date: Sun, 13 Jan 2019 05:59:12 -0500 Subject: [PATCH 144/235] Change alarm panel code format (#20037) * Change code format * Update elkm1 code format * Update alarmdecodes code_format * Update alarmdotcom code_format * Update concord232 code_format * Update envisalink code_format * Update ialarm code_format * Update ifttt code_format * Update manual alarm code_format * Update manual mqtt code_format * Update mqtt code_format * Update ness code_format * Update nx584 code_format * Update satel_integra code_format * Update simplisafe code_format * Update verisure code_format * Change text to be consistent with the Polymer PR --- homeassistant/components/alarm_control_panel/__init__.py | 2 ++ .../components/alarm_control_panel/alarmdecoder.py | 2 +- homeassistant/components/alarm_control_panel/alarmdotcom.py | 2 +- homeassistant/components/alarm_control_panel/concord232.py | 2 +- homeassistant/components/alarm_control_panel/elkm1.py | 2 +- homeassistant/components/alarm_control_panel/envisalink.py | 2 +- homeassistant/components/alarm_control_panel/ialarm.py | 2 +- homeassistant/components/alarm_control_panel/ifttt.py | 2 +- homeassistant/components/alarm_control_panel/manual.py | 2 +- homeassistant/components/alarm_control_panel/manual_mqtt.py | 2 +- homeassistant/components/alarm_control_panel/mqtt.py | 2 +- homeassistant/components/alarm_control_panel/ness_alarm.py | 2 +- homeassistant/components/alarm_control_panel/nx584.py | 2 +- .../components/alarm_control_panel/satel_integra.py | 2 +- homeassistant/components/alarm_control_panel/simplisafe.py | 6 +++--- homeassistant/components/alarm_control_panel/verisure.py | 2 +- 16 files changed, 19 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/alarm_control_panel/__init__.py b/homeassistant/components/alarm_control_panel/__init__.py index ad8520118b4..d1e64dc56d7 100644 --- a/homeassistant/components/alarm_control_panel/__init__.py +++ b/homeassistant/components/alarm_control_panel/__init__.py @@ -21,6 +21,8 @@ from homeassistant.helpers.entity_component import EntityComponent DOMAIN = 'alarm_control_panel' SCAN_INTERVAL = timedelta(seconds=30) ATTR_CHANGED_BY = 'changed_by' +FORMAT_TEXT = 'Text' +FORMAT_NUMBER = 'Number' ENTITY_ID_FORMAT = DOMAIN + '.{}' diff --git a/homeassistant/components/alarm_control_panel/alarmdecoder.py b/homeassistant/components/alarm_control_panel/alarmdecoder.py index 25496dff0eb..16e82280433 100644 --- a/homeassistant/components/alarm_control_panel/alarmdecoder.py +++ b/homeassistant/components/alarm_control_panel/alarmdecoder.py @@ -99,7 +99,7 @@ class AlarmDecoderAlarmPanel(alarm.AlarmControlPanel): @property def code_format(self): """Return one or more digits/characters.""" - return 'Number' + return alarm.FORMAT_NUMBER @property def state(self): diff --git a/homeassistant/components/alarm_control_panel/alarmdotcom.py b/homeassistant/components/alarm_control_panel/alarmdotcom.py index 9b07dc41690..a650e594d22 100644 --- a/homeassistant/components/alarm_control_panel/alarmdotcom.py +++ b/homeassistant/components/alarm_control_panel/alarmdotcom.py @@ -81,7 +81,7 @@ class AlarmDotCom(alarm.AlarmControlPanel): if self._code is None: return None if isinstance(self._code, str) and re.search('^\\d+$', self._code): - return 'Number' + return alarm.FORMAT_NUMBER return 'Any' @property diff --git a/homeassistant/components/alarm_control_panel/concord232.py b/homeassistant/components/alarm_control_panel/concord232.py index e3c2b4a7ec7..015b3cfce33 100644 --- a/homeassistant/components/alarm_control_panel/concord232.py +++ b/homeassistant/components/alarm_control_panel/concord232.py @@ -80,7 +80,7 @@ class Concord232Alarm(alarm.AlarmControlPanel): @property def code_format(self): """Return the characters if code is defined.""" - return 'Number' + return alarm.FORMAT_NUMBER @property def state(self): diff --git a/homeassistant/components/alarm_control_panel/elkm1.py b/homeassistant/components/alarm_control_panel/elkm1.py index 7b8d2e4ac42..c6405f953fd 100644 --- a/homeassistant/components/alarm_control_panel/elkm1.py +++ b/homeassistant/components/alarm_control_panel/elkm1.py @@ -116,7 +116,7 @@ class ElkArea(ElkEntity, alarm.AlarmControlPanel): @property def code_format(self): """Return the alarm code format.""" - return '^[0-9]{4}([0-9]{2})?$' + return alarm.FORMAT_NUMBER @property def state(self): diff --git a/homeassistant/components/alarm_control_panel/envisalink.py b/homeassistant/components/alarm_control_panel/envisalink.py index f0f3d2a43f7..9b772d9bdf0 100644 --- a/homeassistant/components/alarm_control_panel/envisalink.py +++ b/homeassistant/components/alarm_control_panel/envisalink.py @@ -104,7 +104,7 @@ class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel): """Regex for code format or None if no code is required.""" if self._code: return None - return 'Number' + return alarm.FORMAT_NUMBER @property def state(self): diff --git a/homeassistant/components/alarm_control_panel/ialarm.py b/homeassistant/components/alarm_control_panel/ialarm.py index 6115edf406e..abb1862431e 100644 --- a/homeassistant/components/alarm_control_panel/ialarm.py +++ b/homeassistant/components/alarm_control_panel/ialarm.py @@ -82,7 +82,7 @@ class IAlarmPanel(alarm.AlarmControlPanel): if self._code is None: return None if isinstance(self._code, str) and re.search('^\\d+$', self._code): - return 'Number' + return alarm.FORMAT_NUMBER return 'Any' @property diff --git a/homeassistant/components/alarm_control_panel/ifttt.py b/homeassistant/components/alarm_control_panel/ifttt.py index 49c5dc488c0..f88f7b3dfab 100644 --- a/homeassistant/components/alarm_control_panel/ifttt.py +++ b/homeassistant/components/alarm_control_panel/ifttt.py @@ -129,7 +129,7 @@ class IFTTTAlarmPanel(alarm.AlarmControlPanel): if self._code is None: return None if isinstance(self._code, str) and re.search('^\\d+$', self._code): - return 'Number' + return alarm.FORMAT_NUMBER return 'Any' def alarm_disarm(self, code=None): diff --git a/homeassistant/components/alarm_control_panel/manual.py b/homeassistant/components/alarm_control_panel/manual.py index 0bbbd0689e2..1efed8ebb7b 100644 --- a/homeassistant/components/alarm_control_panel/manual.py +++ b/homeassistant/components/alarm_control_panel/manual.py @@ -207,7 +207,7 @@ class ManualAlarm(alarm.AlarmControlPanel, RestoreEntity): if self._code is None: return None if isinstance(self._code, str) and re.search('^\\d+$', self._code): - return 'Number' + return alarm.FORMAT_NUMBER return 'Any' def alarm_disarm(self, code=None): diff --git a/homeassistant/components/alarm_control_panel/manual_mqtt.py b/homeassistant/components/alarm_control_panel/manual_mqtt.py index fc59ac4d088..48b8c1053c4 100644 --- a/homeassistant/components/alarm_control_panel/manual_mqtt.py +++ b/homeassistant/components/alarm_control_panel/manual_mqtt.py @@ -241,7 +241,7 @@ class ManualMQTTAlarm(alarm.AlarmControlPanel): if self._code is None: return None if isinstance(self._code, str) and re.search('^\\d+$', self._code): - return 'Number' + return alarm.FORMAT_NUMBER return 'Any' def alarm_disarm(self, code=None): diff --git a/homeassistant/components/alarm_control_panel/mqtt.py b/homeassistant/components/alarm_control_panel/mqtt.py index 0113cfa93b1..df583fbbd94 100644 --- a/homeassistant/components/alarm_control_panel/mqtt.py +++ b/homeassistant/components/alarm_control_panel/mqtt.py @@ -172,7 +172,7 @@ class MqttAlarm(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, if code is None: return None if isinstance(code, str) and re.search('^\\d+$', code): - return 'Number' + return alarm.FORMAT_NUMBER return 'Any' async def async_alarm_disarm(self, code=None): diff --git a/homeassistant/components/alarm_control_panel/ness_alarm.py b/homeassistant/components/alarm_control_panel/ness_alarm.py index ec52ef51e2f..ee3a0c213cb 100644 --- a/homeassistant/components/alarm_control_panel/ness_alarm.py +++ b/homeassistant/components/alarm_control_panel/ness_alarm.py @@ -59,7 +59,7 @@ class NessAlarmPanel(alarm.AlarmControlPanel): @property def code_format(self): """Return the regex for code format or None if no code is required.""" - return 'Number' + return alarm.FORMAT_NUMBER @property def state(self): diff --git a/homeassistant/components/alarm_control_panel/nx584.py b/homeassistant/components/alarm_control_panel/nx584.py index 1b3e86c4ca6..c84872d0b25 100644 --- a/homeassistant/components/alarm_control_panel/nx584.py +++ b/homeassistant/components/alarm_control_panel/nx584.py @@ -70,7 +70,7 @@ class NX584Alarm(alarm.AlarmControlPanel): @property def code_format(self): """Return one or more digits/characters.""" - return 'Number' + return alarm.FORMAT_NUMBER @property def state(self): diff --git a/homeassistant/components/alarm_control_panel/satel_integra.py b/homeassistant/components/alarm_control_panel/satel_integra.py index c4e42855d8a..b704677800f 100644 --- a/homeassistant/components/alarm_control_panel/satel_integra.py +++ b/homeassistant/components/alarm_control_panel/satel_integra.py @@ -64,7 +64,7 @@ class SatelIntegraAlarmPanel(alarm.AlarmControlPanel): @property def code_format(self): """Return the regex for code format or None if no code is required.""" - return 'Number' + return alarm.FORMAT_NUMBER @property def state(self): diff --git a/homeassistant/components/alarm_control_panel/simplisafe.py b/homeassistant/components/alarm_control_panel/simplisafe.py index cdcdf07c982..cf470f15c56 100644 --- a/homeassistant/components/alarm_control_panel/simplisafe.py +++ b/homeassistant/components/alarm_control_panel/simplisafe.py @@ -7,7 +7,7 @@ https://home-assistant.io/components/alarm_control_panel.simplisafe/ import logging import re -from homeassistant.components.alarm_control_panel import AlarmControlPanel +import homeassistant.components.alarm_control_panel as alarm from homeassistant.components.simplisafe.const import ( DATA_CLIENT, DOMAIN, TOPIC_UPDATE) from homeassistant.const import ( @@ -37,7 +37,7 @@ async def async_setup_entry(hass, entry, async_add_entities): ], True) -class SimpliSafeAlarm(AlarmControlPanel): +class SimpliSafeAlarm(alarm.AlarmControlPanel): """Representation of a SimpliSafe alarm.""" def __init__(self, system, code): @@ -64,7 +64,7 @@ class SimpliSafeAlarm(AlarmControlPanel): if not self._code: return None if isinstance(self._code, str) and re.search('^\\d+$', self._code): - return 'Number' + return alarm.FORMAT_NUMBER return 'Any' @property diff --git a/homeassistant/components/alarm_control_panel/verisure.py b/homeassistant/components/alarm_control_panel/verisure.py index f5a631df390..6b381ef5a47 100644 --- a/homeassistant/components/alarm_control_panel/verisure.py +++ b/homeassistant/components/alarm_control_panel/verisure.py @@ -61,7 +61,7 @@ class VerisureAlarm(alarm.AlarmControlPanel): @property def code_format(self): """Return one or more digits/characters.""" - return 'Number' + return alarm.FORMAT_NUMBER @property def changed_by(self): From aae6ff830a9a3042a4e863c5f0bf5c6e890aab4c Mon Sep 17 00:00:00 2001 From: carstenschroeder Date: Sun, 13 Jan 2019 14:23:22 +0100 Subject: [PATCH 145/235] ADS service: Enable use of templates for value (#20024) --- homeassistant/components/ads/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/ads/__init__.py b/homeassistant/components/ads/__init__.py index 657ff8acc3a..360236790f8 100644 --- a/homeassistant/components/ads/__init__.py +++ b/homeassistant/components/ads/__init__.py @@ -47,7 +47,7 @@ CONFIG_SCHEMA = vol.Schema({ SCHEMA_SERVICE_WRITE_DATA_BY_NAME = vol.Schema({ vol.Required(CONF_ADS_TYPE): vol.In([ADSTYPE_INT, ADSTYPE_UINT, ADSTYPE_BYTE, ADSTYPE_BOOL]), - vol.Required(CONF_ADS_VALUE): cv.match_all, + vol.Required(CONF_ADS_VALUE): vol.Coerce(int), vol.Required(CONF_ADS_VAR): cv.string, }) From 2339cb05ad9f9ac5e49eb095245bf4490b7cbf79 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Sun, 13 Jan 2019 15:52:23 +0100 Subject: [PATCH 146/235] Fix errors in ESPHome integration (#20048) * Fix Home Assistant State Import * Fix cover state * Fix fan supported features * Fix typo --- homeassistant/components/cover/esphome.py | 9 +-------- homeassistant/components/esphome/__init__.py | 8 ++++---- homeassistant/components/fan/esphome.py | 8 +++++++- 3 files changed, 12 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/cover/esphome.py b/homeassistant/components/cover/esphome.py index 97f082f8be5..14fce3fb4eb 100644 --- a/homeassistant/components/cover/esphome.py +++ b/homeassistant/components/cover/esphome.py @@ -8,7 +8,6 @@ from homeassistant.components.cover import CoverDevice, SUPPORT_CLOSE, \ from homeassistant.components.esphome import EsphomeEntity, \ platform_async_setup_entry from homeassistant.config_entries import ConfigEntry -from homeassistant.const import STATE_CLOSED, STATE_OPEN from homeassistant.helpers.typing import HomeAssistantType if TYPE_CHECKING: @@ -33,12 +32,6 @@ async def async_setup_entry(hass: HomeAssistantType, ) -COVER_STATE_INT_TO_STR = { - 0: STATE_OPEN, - 1: STATE_CLOSED -} - - class EsphomeCover(EsphomeEntity, CoverDevice): """A cover implementation for ESPHome.""" @@ -65,7 +58,7 @@ class EsphomeCover(EsphomeEntity, CoverDevice): """Return if the cover is closed or not.""" if self._state is None: return None - return COVER_STATE_INT_TO_STR[self._state.state] + return bool(self._state.state) async def async_open_cover(self, **kwargs) -> None: """Open the cover.""" diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index 70d92250564..7578f2e244f 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -10,7 +10,7 @@ from homeassistant import const from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, \ EVENT_HOMEASSISTANT_STOP -from homeassistant.core import callback, Event +from homeassistant.core import callback, Event, State import homeassistant.helpers.device_registry as dr from homeassistant.exceptions import TemplateError from homeassistant.helpers import template @@ -208,11 +208,11 @@ async def async_setup_entry(hass: HomeAssistantType, domain, service_name, service_data, blocking=True)) async def send_home_assistant_state(entity_id: str, _, - new_state: Optional[str]) -> None: + new_state: Optional[State]) -> None: """Forward Home Assistant states to ESPHome.""" if new_state is None: return - await cli.send_home_assistant_state(entity_id, new_state) + await cli.send_home_assistant_state(entity_id, new_state.state) @callback def async_on_state_subscription(entity_id: str) -> None: @@ -481,7 +481,7 @@ class EsphomeEntity(Entity): self._remove_callbacks.append( async_dispatcher_connect(self.hass, DISPATCHER_REMOVE_ENTITY.format(**kwargs), - self.async_schedule_update_ha_state) + self.async_remove) ) self._remove_callbacks.append( diff --git a/homeassistant/components/fan/esphome.py b/homeassistant/components/fan/esphome.py index a2a3d6263f8..49e2401545b 100644 --- a/homeassistant/components/fan/esphome.py +++ b/homeassistant/components/fan/esphome.py @@ -91,6 +91,8 @@ class EsphomeFan(EsphomeEntity, FanEntity): """Return the current speed.""" if self._state is None: return None + if not self._static_info.supports_speed: + return None return FAN_SPEED_INT_TO_STR[self._state.speed] @property @@ -98,11 +100,15 @@ class EsphomeFan(EsphomeEntity, FanEntity): """Return the oscillation state.""" if self._state is None: return None + if not self._static_info.supports_oscillation: + return None return self._state.oscillating @property - def speed_list(self) -> List[str]: + def speed_list(self) -> Optional[List[str]]: """Get the list of available speeds.""" + if not self._static_info.supports_speed: + return None return [SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH] @property From 4af4b2d10e520bdbe282c3d04a1942d221e4bf87 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Sun, 13 Jan 2019 16:39:50 +0100 Subject: [PATCH 147/235] Fix remote.harmony_change_channel services.yaml indentation (#20051) --- homeassistant/components/remote/services.yaml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/remote/services.yaml b/homeassistant/components/remote/services.yaml index ea3a48e253a..1fb4b048707 100644 --- a/homeassistant/components/remote/services.yaml +++ b/homeassistant/components/remote/services.yaml @@ -52,13 +52,13 @@ harmony_sync: harmony_change_channel: description: Sends change channel command to the Harmony HUB - fields: - entity_id: - description: Name(s) of Harmony remote entities to send change channel command to - example: 'remote.family_room' - channel: - description: Channel number to change to - example: '200' + fields: + entity_id: + description: Name(s) of Harmony remote entities to send change channel command to + example: 'remote.family_room' + channel: + description: Channel number to change to + example: '200' xiaomi_miio_learn_command: description: 'Learn an IR command, press "Call Service", point the remote at the IR device, and the learned command will be shown as a notification in Overview.' From 96b8c517f0456e6a007d26f0e53dedca76ac03bf Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 13 Jan 2019 09:38:22 -0800 Subject: [PATCH 148/235] Update translations --- .../components/auth/.translations/et.json | 7 ++++++ .../components/cast/.translations/et.json | 10 +++++++++ .../components/deconz/.translations/et.json | 13 +++++++++++ .../dialogflow/.translations/ca.json | 2 +- .../emulated_roku/.translations/ca.json | 21 ++++++++++++++++++ .../emulated_roku/.translations/et.json | 13 +++++++++++ .../emulated_roku/.translations/ko.json | 21 ++++++++++++++++++ .../emulated_roku/.translations/no.json | 21 ++++++++++++++++++ .../emulated_roku/.translations/ru.json | 18 +++++++++++++++ .../components/esphome/.translations/ru.json | 2 +- .../components/geofency/.translations/ca.json | 18 +++++++++++++++ .../components/geofency/.translations/ko.json | 18 +++++++++++++++ .../components/geofency/.translations/no.json | 18 +++++++++++++++ .../components/geofency/.translations/ru.json | 18 +++++++++++++++ .../components/hangouts/.translations/et.json | 22 +++++++++++++++++++ .../homematicip_cloud/.translations/et.json | 16 ++++++++++++++ .../components/hue/.translations/et.json | 15 +++++++++++++ .../components/ifttt/.translations/ca.json | 2 +- .../components/ifttt/.translations/et.json | 5 +++++ .../components/ios/.translations/et.json | 10 +++++++++ .../components/mailgun/.translations/ca.json | 2 +- .../components/mqtt/.translations/et.json | 10 +++++++++ .../components/nest/.translations/et.json | 15 +++++++++++++ .../components/nest/.translations/fr.json | 2 +- .../components/nest/.translations/ru.json | 2 +- .../sensor/.translations/moon.de.json | 2 +- .../sensor/.translations/moon.et.json | 12 ++++++++++ .../sensor/.translations/moon.sl.json | 2 +- .../sensor/.translations/season.et.json | 8 +++++++ .../components/sonos/.translations/et.json | 10 +++++++++ .../components/tradfri/.translations/et.json | 11 ++++++++++ .../components/twilio/.translations/ca.json | 2 +- .../components/upnp/.translations/et.json | 15 +++++++++++++ .../components/zone/.translations/et.json | 16 ++++++++++++++ .../components/zwave/.translations/et.json | 5 +++++ 35 files changed, 375 insertions(+), 9 deletions(-) create mode 100644 homeassistant/components/auth/.translations/et.json create mode 100644 homeassistant/components/cast/.translations/et.json create mode 100644 homeassistant/components/deconz/.translations/et.json create mode 100644 homeassistant/components/emulated_roku/.translations/ca.json create mode 100644 homeassistant/components/emulated_roku/.translations/et.json create mode 100644 homeassistant/components/emulated_roku/.translations/ko.json create mode 100644 homeassistant/components/emulated_roku/.translations/no.json create mode 100644 homeassistant/components/emulated_roku/.translations/ru.json create mode 100644 homeassistant/components/geofency/.translations/ca.json create mode 100644 homeassistant/components/geofency/.translations/ko.json create mode 100644 homeassistant/components/geofency/.translations/no.json create mode 100644 homeassistant/components/geofency/.translations/ru.json create mode 100644 homeassistant/components/hangouts/.translations/et.json create mode 100644 homeassistant/components/homematicip_cloud/.translations/et.json create mode 100644 homeassistant/components/hue/.translations/et.json create mode 100644 homeassistant/components/ifttt/.translations/et.json create mode 100644 homeassistant/components/ios/.translations/et.json create mode 100644 homeassistant/components/mqtt/.translations/et.json create mode 100644 homeassistant/components/nest/.translations/et.json create mode 100644 homeassistant/components/sensor/.translations/moon.et.json create mode 100644 homeassistant/components/sensor/.translations/season.et.json create mode 100644 homeassistant/components/sonos/.translations/et.json create mode 100644 homeassistant/components/tradfri/.translations/et.json create mode 100644 homeassistant/components/upnp/.translations/et.json create mode 100644 homeassistant/components/zone/.translations/et.json create mode 100644 homeassistant/components/zwave/.translations/et.json diff --git a/homeassistant/components/auth/.translations/et.json b/homeassistant/components/auth/.translations/et.json new file mode 100644 index 00000000000..290f4ee12a9 --- /dev/null +++ b/homeassistant/components/auth/.translations/et.json @@ -0,0 +1,7 @@ +{ + "mfa_setup": { + "totp": { + "title": "" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cast/.translations/et.json b/homeassistant/components/cast/.translations/et.json new file mode 100644 index 00000000000..987c54955f2 --- /dev/null +++ b/homeassistant/components/cast/.translations/et.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "confirm": { + "title": "" + } + }, + "title": "" + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/et.json b/homeassistant/components/deconz/.translations/et.json new file mode 100644 index 00000000000..93c54b3915c --- /dev/null +++ b/homeassistant/components/deconz/.translations/et.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "init": { + "data": { + "host": "", + "port": "" + } + } + }, + "title": "deCONZ Zigbee l\u00fc\u00fcs" + } +} \ No newline at end of file diff --git a/homeassistant/components/dialogflow/.translations/ca.json b/homeassistant/components/dialogflow/.translations/ca.json index f6dfc9399c2..0967b1c158e 100644 --- a/homeassistant/components/dialogflow/.translations/ca.json +++ b/homeassistant/components/dialogflow/.translations/ca.json @@ -10,7 +10,7 @@ "step": { "user": { "description": "Est\u00e0s segur que vols configurar Dialogflow?", - "title": "Configuraci\u00f3 del Webhook de Dialogflow" + "title": "Configuraci\u00f3 del Webhook Dialogflow" } }, "title": "Dialogflow" diff --git a/homeassistant/components/emulated_roku/.translations/ca.json b/homeassistant/components/emulated_roku/.translations/ca.json new file mode 100644 index 00000000000..bdd38b8538c --- /dev/null +++ b/homeassistant/components/emulated_roku/.translations/ca.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "name_exists": "El nom ja existeix" + }, + "step": { + "user": { + "data": { + "advertise_ip": "IP d'advert\u00e8ncies", + "advertise_port": "Port d'advert\u00e8ncies", + "host_ip": "IP de l'amfitri\u00f3", + "listen_port": "Port d'escolta", + "name": "Nom", + "upnp_bind_multicast": "Enlla\u00e7ar multicast (true/false)" + }, + "title": "Configuraci\u00f3 del servidor" + } + }, + "title": "EmulatedRoku" + } +} \ No newline at end of file diff --git a/homeassistant/components/emulated_roku/.translations/et.json b/homeassistant/components/emulated_roku/.translations/et.json new file mode 100644 index 00000000000..e284f6c3732 --- /dev/null +++ b/homeassistant/components/emulated_roku/.translations/et.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host_ip": "", + "name": "Nimi" + } + } + }, + "title": "" + } +} \ No newline at end of file diff --git a/homeassistant/components/emulated_roku/.translations/ko.json b/homeassistant/components/emulated_roku/.translations/ko.json new file mode 100644 index 00000000000..54c3e079386 --- /dev/null +++ b/homeassistant/components/emulated_roku/.translations/ko.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "name_exists": "\uc774\ub984\uc774 \uc774\ubbf8 \uc874\uc7ac\ud569\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "advertise_ip": "\uad11\uace0 IP", + "advertise_port": "\uad11\uace0 \ud3ec\ud2b8", + "host_ip": "\ud638\uc2a4\ud2b8 IP", + "listen_port": "\uc218\uc2e0 \ud3ec\ud2b8", + "name": "\uc774\ub984", + "upnp_bind_multicast": "\uba40\ud2f0 \uce90\uc2a4\ud2b8 \ubc14\uc778\ub4dc (\ucc38/\uac70\uc9d3)" + }, + "title": "\uc11c\ubc84 \uad6c\uc131 \uc815\uc758" + } + }, + "title": "EmulatedRoku" + } +} \ No newline at end of file diff --git a/homeassistant/components/emulated_roku/.translations/no.json b/homeassistant/components/emulated_roku/.translations/no.json new file mode 100644 index 00000000000..e83497599ca --- /dev/null +++ b/homeassistant/components/emulated_roku/.translations/no.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "name_exists": "Navnet eksisterer allerede" + }, + "step": { + "user": { + "data": { + "advertise_ip": "Annonser IP", + "advertise_port": "Annonser port", + "host_ip": "Vert IP", + "listen_port": "Lytte port", + "name": "Navn", + "upnp_bind_multicast": "Bind multicast (True/False)" + }, + "title": "Definer serverkonfigurasjon" + } + }, + "title": "EmulatedRoku" + } +} \ No newline at end of file diff --git a/homeassistant/components/emulated_roku/.translations/ru.json b/homeassistant/components/emulated_roku/.translations/ru.json new file mode 100644 index 00000000000..611b5647233 --- /dev/null +++ b/homeassistant/components/emulated_roku/.translations/ru.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "name_exists": "\u0418\u043c\u044f \u0443\u0436\u0435 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u0435\u0442" + }, + "step": { + "user": { + "data": { + "host_ip": "\u0425\u043e\u0441\u0442", + "listen_port": "\u041f\u043e\u0440\u0442", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435" + }, + "title": "EmulatedRoku" + } + }, + "title": "EmulatedRoku" + } +} \ No newline at end of file diff --git a/homeassistant/components/esphome/.translations/ru.json b/homeassistant/components/esphome/.translations/ru.json index 4b227bd1565..fc74825e188 100644 --- a/homeassistant/components/esphome/.translations/ru.json +++ b/homeassistant/components/esphome/.translations/ru.json @@ -6,7 +6,7 @@ "error": { "connection_error": "\u041d\u0435 \u0443\u0434\u0430\u0435\u0442\u0441\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a ESP. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0443\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e \u0432\u0430\u0448 YAML-\u0444\u0430\u0439\u043b \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u0442 \u0441\u0442\u0440\u043e\u043a\u0443 'api:'.", "invalid_password": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043f\u0430\u0440\u043e\u043b\u044c!", - "resolve_error": "\u041d\u0435 \u0443\u0434\u0430\u0435\u0442\u0441\u044f \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u0442\u044c \u0430\u0434\u0440\u0435\u0441 ESP. \u0415\u0441\u043b\u0438 \u044d\u0442\u0430 \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0432\u0442\u043e\u0440\u044f\u0435\u0442\u0441\u044f, \u0443\u043a\u0430\u0436\u0438\u0442\u0435 [\u0441\u0442\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438\u0439 IP-\u0430\u0434\u0440\u0435\u0441](https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips)" + "resolve_error": "\u041d\u0435 \u0443\u0434\u0430\u0435\u0442\u0441\u044f \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u0442\u044c \u0430\u0434\u0440\u0435\u0441 ESP. \u0415\u0441\u043b\u0438 \u044d\u0442\u0430 \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0432\u0442\u043e\u0440\u044f\u0435\u0442\u0441\u044f, \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 \u0441\u0442\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438\u0439 IP-\u0430\u0434\u0440\u0435\u0441: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" }, "step": { "authenticate": { diff --git a/homeassistant/components/geofency/.translations/ca.json b/homeassistant/components/geofency/.translations/ca.json new file mode 100644 index 00000000000..125ca51399a --- /dev/null +++ b/homeassistant/components/geofency/.translations/ca.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "La inst\u00e0ncia de Home Assistant ha de ser accessible des d'Internet per rebre missatges de Geofency.", + "one_instance_allowed": "Nom\u00e9s cal una sola inst\u00e0ncia." + }, + "create_entry": { + "default": "Per enviar esdeveniments a Home Assistant, haur\u00e0s de configurar l'opci\u00f3 webhook de Geofency.\n\nCompleta la seg\u00fcent informaci\u00f3:\n\n- URL: `{webhook_url}` \n- M\u00e8tode: POST \n\nConsulta la [documentaci\u00f3]({docs_url}) per a m\u00e9s detalls." + }, + "step": { + "user": { + "description": "Est\u00e0s segur que vols configurar el Webhook Geofency?", + "title": "Configuraci\u00f3 del Webhook Geofency" + } + }, + "title": "Webhook Geofency" + } +} \ No newline at end of file diff --git a/homeassistant/components/geofency/.translations/ko.json b/homeassistant/components/geofency/.translations/ko.json new file mode 100644 index 00000000000..8a857acbdc6 --- /dev/null +++ b/homeassistant/components/geofency/.translations/ko.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Geofency \uba54\uc2dc\uc9c0\ub97c \ubc1b\uc73c\ub824\uba74 \uc778\ud130\ub137\uc5d0\uc11c Home Assistant \uc778\uc2a4\ud134\uc2a4\uc5d0 \uc561\uc138\uc2a4 \ud560 \uc218 \uc788\uc5b4\uc57c \ud569\ub2c8\ub2e4.", + "one_instance_allowed": "\ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \ud544\uc694\ud569\ub2c8\ub2e4." + }, + "create_entry": { + "default": "Home Assistant \ub85c \uc774\ubca4\ud2b8\ub97c \ubcf4\ub0b4\ub824\uba74 Geofency \uc5d0\uc11c Webhook \uc744 \uc124\uc815\ud574\uc57c\ud569\ub2c8\ub2e4. \n\n\ub2e4\uc74c \uc815\ubcf4\ub97c \uc785\ub825\ud574 \uc8fc\uc138\uc694. \n\n - URL: `{webhook_url}`\n - Method: POST\n \n \uc790\uc138\ud55c \uc815\ubcf4\ub294 [\uc548\ub0b4]({docs_url}) \ub97c \ucc38\uc870\ud574 \uc8fc\uc138\uc694." + }, + "step": { + "user": { + "description": "Geofency Webhook \uc744 \uc124\uc815 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "title": "Geofency Webhook \uc124\uc815" + } + }, + "title": "Geofency Webhook" + } +} \ No newline at end of file diff --git a/homeassistant/components/geofency/.translations/no.json b/homeassistant/components/geofency/.translations/no.json new file mode 100644 index 00000000000..4409616cef4 --- /dev/null +++ b/homeassistant/components/geofency/.translations/no.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Home Assistant m\u00e5 v\u00e6re tilgjengelig fra internett for \u00e5 kunne motta meldinger fra Geofency.", + "one_instance_allowed": "Kun en enkelt forekomst er n\u00f8dvendig." + }, + "create_entry": { + "default": "For \u00e5 kunne sende hendelser til Home Assistant, m\u00e5 du sette opp webhook-funksjonen i Geofency. \n\n Fyll ut f\u00f8lgende informasjon: \n\n - URL: `{webhook_url}` \n - Metode: POST \n\n Se [dokumentasjonen]({docs_url}) for ytterligere detaljer." + }, + "step": { + "user": { + "description": "Er du sikker p\u00e5 at du vil konfigurere Geofency Webhook?", + "title": "Sett opp Geofency Webhook" + } + }, + "title": "Geofency Webhook" + } +} \ No newline at end of file diff --git a/homeassistant/components/geofency/.translations/ru.json b/homeassistant/components/geofency/.translations/ru.json new file mode 100644 index 00000000000..34290b35f42 --- /dev/null +++ b/homeassistant/components/geofency/.translations/ru.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "\u0412\u0430\u0448 Home Assistant \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d \u0438\u0437 \u0438\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u0430 \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0439 Geofency.", + "one_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." + }, + "create_entry": { + "default": "\u0414\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u0432 Home Assistant \u0432\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c webhooks \u0434\u043b\u044f Geofency.\n\n\u0414\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0443\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e:\n\n- URL: `{webhook_url}`\n- Method: POST\n\n\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438." + }, + "step": { + "user": { + "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Geofency Webhook?", + "title": "Geofency Webhook" + } + }, + "title": "Geofency Webhook" + } +} \ No newline at end of file diff --git a/homeassistant/components/hangouts/.translations/et.json b/homeassistant/components/hangouts/.translations/et.json new file mode 100644 index 00000000000..4bd26876ac6 --- /dev/null +++ b/homeassistant/components/hangouts/.translations/et.json @@ -0,0 +1,22 @@ +{ + "config": { + "error": { + "invalid_login": "Vale Kasutajanimi, palun proovige uuesti." + }, + "step": { + "2fa": { + "data": { + "2fa": "2FA PIN" + }, + "title": "Kaheastmeline autentimine" + }, + "user": { + "data": { + "email": "E-posti aadress", + "password": "Salas\u00f5na" + } + } + }, + "title": "" + } +} \ No newline at end of file diff --git a/homeassistant/components/homematicip_cloud/.translations/et.json b/homeassistant/components/homematicip_cloud/.translations/et.json new file mode 100644 index 00000000000..7aedd80b5d0 --- /dev/null +++ b/homeassistant/components/homematicip_cloud/.translations/et.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "invalid_pin": "Vale PIN, palun proovige uuesti" + }, + "step": { + "init": { + "data": { + "hapid": "P\u00e4\u00e4supunkti ID (SGTIN)", + "pin": "PIN-kood (valikuline)" + } + } + }, + "title": "HomematicIP Pilv" + } +} \ No newline at end of file diff --git a/homeassistant/components/hue/.translations/et.json b/homeassistant/components/hue/.translations/et.json new file mode 100644 index 00000000000..6bad10ed067 --- /dev/null +++ b/homeassistant/components/hue/.translations/et.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "unknown": "Ilmnes tundmatu viga" + }, + "step": { + "init": { + "data": { + "host": "" + } + } + }, + "title": "" + } +} \ No newline at end of file diff --git a/homeassistant/components/ifttt/.translations/ca.json b/homeassistant/components/ifttt/.translations/ca.json index 597328a2ee4..ff4cf67c23b 100644 --- a/homeassistant/components/ifttt/.translations/ca.json +++ b/homeassistant/components/ifttt/.translations/ca.json @@ -10,7 +10,7 @@ "step": { "user": { "description": "Est\u00e0s segur que vols configurar IFTTT?", - "title": "Configuraci\u00f3 de la miniaplicaci\u00f3 Webhook de IFTTT" + "title": "Configuraci\u00f3 de la miniaplicaci\u00f3 Webhook IFTTT" } }, "title": "IFTTT" diff --git a/homeassistant/components/ifttt/.translations/et.json b/homeassistant/components/ifttt/.translations/et.json new file mode 100644 index 00000000000..8c4c45f9c89 --- /dev/null +++ b/homeassistant/components/ifttt/.translations/et.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "" + } +} \ No newline at end of file diff --git a/homeassistant/components/ios/.translations/et.json b/homeassistant/components/ios/.translations/et.json new file mode 100644 index 00000000000..987c54955f2 --- /dev/null +++ b/homeassistant/components/ios/.translations/et.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "confirm": { + "title": "" + } + }, + "title": "" + } +} \ No newline at end of file diff --git a/homeassistant/components/mailgun/.translations/ca.json b/homeassistant/components/mailgun/.translations/ca.json index f43467de7d9..8815360f7b4 100644 --- a/homeassistant/components/mailgun/.translations/ca.json +++ b/homeassistant/components/mailgun/.translations/ca.json @@ -10,7 +10,7 @@ "step": { "user": { "description": "Est\u00e0s segur que vols configurar Mailgun?", - "title": "Configuraci\u00f3 del Webhook de Mailgun" + "title": "Configuraci\u00f3 del Webhook Mailgun" } }, "title": "Mailgun" diff --git a/homeassistant/components/mqtt/.translations/et.json b/homeassistant/components/mqtt/.translations/et.json new file mode 100644 index 00000000000..4ba36fd3361 --- /dev/null +++ b/homeassistant/components/mqtt/.translations/et.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "broker": { + "title": "" + } + }, + "title": "" + } +} \ No newline at end of file diff --git a/homeassistant/components/nest/.translations/et.json b/homeassistant/components/nest/.translations/et.json new file mode 100644 index 00000000000..4e8c0b23bdc --- /dev/null +++ b/homeassistant/components/nest/.translations/et.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "invalid_code": "Kehtetu kood" + }, + "step": { + "link": { + "data": { + "code": "PIN-kood" + } + } + }, + "title": "" + } +} \ No newline at end of file diff --git a/homeassistant/components/nest/.translations/fr.json b/homeassistant/components/nest/.translations/fr.json index 822ec6ae836..3cd7b003f66 100644 --- a/homeassistant/components/nest/.translations/fr.json +++ b/homeassistant/components/nest/.translations/fr.json @@ -24,7 +24,7 @@ "data": { "code": "Code PIN" }, - "description": "Pour associer votre compte Nest, [autorisez votre compte] ( {url} ). \n\n Apr\u00e8s autorisation, copiez-collez le code PIN fourni ci-dessous.", + "description": "Pour associer votre compte Nest, [autorisez votre compte]({url}). \n\n Apr\u00e8s autorisation, copiez-collez le code PIN fourni ci-dessous.", "title": "Lier un compte Nest" } }, diff --git a/homeassistant/components/nest/.translations/ru.json b/homeassistant/components/nest/.translations/ru.json index 6a73bd47203..54ff1dff999 100644 --- a/homeassistant/components/nest/.translations/ru.json +++ b/homeassistant/components/nest/.translations/ru.json @@ -24,7 +24,7 @@ "data": { "code": "\u041f\u0438\u043d-\u043a\u043e\u0434" }, - "description": " [\u0410\u0432\u0442\u043e\u0440\u0438\u0437\u0443\u0439\u0442\u0435\u0441\u044c]({url}), \u0447\u0442\u043e\u0431\u044b \u043f\u0440\u0438\u0432\u044f\u0437\u0430\u0442\u044c \u0441\u0432\u043e\u044e \u0443\u0447\u0435\u0442\u043d\u0443\u044e \u0437\u0430\u043f\u0438\u0441\u044c Nest. \n\n \u041f\u043e\u0441\u043b\u0435 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438 \u0441\u043a\u043e\u043f\u0438\u0440\u0443\u0439\u0442\u0435 \u043f\u0440\u0438\u043b\u0430\u0433\u0430\u0435\u043c\u044b\u0439 \u043f\u0438\u043d-\u043a\u043e\u0434.", + "description": "[\u0410\u0432\u0442\u043e\u0440\u0438\u0437\u0443\u0439\u0442\u0435\u0441\u044c]({url}), \u0447\u0442\u043e\u0431\u044b \u043f\u0440\u0438\u0432\u044f\u0437\u0430\u0442\u044c \u0441\u0432\u043e\u044e \u0443\u0447\u0435\u0442\u043d\u0443\u044e \u0437\u0430\u043f\u0438\u0441\u044c Nest. \n\n \u041f\u043e\u0441\u043b\u0435 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438 \u0441\u043a\u043e\u043f\u0438\u0440\u0443\u0439\u0442\u0435 \u043f\u0440\u0438\u043b\u0430\u0433\u0430\u0435\u043c\u044b\u0439 \u043f\u0438\u043d-\u043a\u043e\u0434.", "title": "\u041f\u0440\u0438\u0432\u044f\u0437\u0430\u0442\u044c \u0443\u0447\u0435\u0442\u043d\u0443\u044e \u0437\u0430\u043f\u0438\u0441\u044c Nest" } }, diff --git a/homeassistant/components/sensor/.translations/moon.de.json b/homeassistant/components/sensor/.translations/moon.de.json index aebca53ec4d..310ebf9c359 100644 --- a/homeassistant/components/sensor/.translations/moon.de.json +++ b/homeassistant/components/sensor/.translations/moon.de.json @@ -6,7 +6,7 @@ "new_moon": "Neumond", "waning_crescent": "Abnehmende Sichel", "waning_gibbous": "Drittes Viertel", - "waxing_crescent": " Zunehmende Sichel", + "waxing_crescent": "Zunehmende Sichel", "waxing_gibbous": "Zweites Viertel" } } \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/moon.et.json b/homeassistant/components/sensor/.translations/moon.et.json new file mode 100644 index 00000000000..0d82e0d8f94 --- /dev/null +++ b/homeassistant/components/sensor/.translations/moon.et.json @@ -0,0 +1,12 @@ +{ + "state": { + "first_quarter": "Esimene veerand", + "full_moon": "T\u00e4iskuu", + "last_quarter": "Viimane veerand", + "new_moon": "Kuu loomine", + "waning_crescent": "Vanakuu", + "waning_gibbous": "Kahanev kuu", + "waxing_crescent": "Noorkuu", + "waxing_gibbous": "Kasvav kuu" + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/moon.sl.json b/homeassistant/components/sensor/.translations/moon.sl.json index 41e873e4def..1b69e10e6f9 100644 --- a/homeassistant/components/sensor/.translations/moon.sl.json +++ b/homeassistant/components/sensor/.translations/moon.sl.json @@ -6,7 +6,7 @@ "new_moon": "Mlaj", "waning_crescent": "Zadnji izbo\u010dec", "waning_gibbous": "Zadnji srpec", - "waxing_crescent": " Prvi izbo\u010dec", + "waxing_crescent": "Prvi izbo\u010dec", "waxing_gibbous": "Prvi srpec" } } \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/season.et.json b/homeassistant/components/sensor/.translations/season.et.json new file mode 100644 index 00000000000..1415a3b907b --- /dev/null +++ b/homeassistant/components/sensor/.translations/season.et.json @@ -0,0 +1,8 @@ +{ + "state": { + "autumn": "S\u00fcgis", + "spring": "Kevad", + "summer": "Suvi", + "winter": "Talv" + } +} \ No newline at end of file diff --git a/homeassistant/components/sonos/.translations/et.json b/homeassistant/components/sonos/.translations/et.json new file mode 100644 index 00000000000..987c54955f2 --- /dev/null +++ b/homeassistant/components/sonos/.translations/et.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "confirm": { + "title": "" + } + }, + "title": "" + } +} \ No newline at end of file diff --git a/homeassistant/components/tradfri/.translations/et.json b/homeassistant/components/tradfri/.translations/et.json new file mode 100644 index 00000000000..5d0a728407a --- /dev/null +++ b/homeassistant/components/tradfri/.translations/et.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "auth": { + "data": { + "host": "" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/twilio/.translations/ca.json b/homeassistant/components/twilio/.translations/ca.json index 324ab0dd69a..796f71ee7e7 100644 --- a/homeassistant/components/twilio/.translations/ca.json +++ b/homeassistant/components/twilio/.translations/ca.json @@ -10,7 +10,7 @@ "step": { "user": { "description": "Est\u00e0s segur que vols configurar Twilio?", - "title": "Configuraci\u00f3 del Webhook de Twilio" + "title": "Configuraci\u00f3 del Webhook Twilio" } }, "title": "Twilio" diff --git a/homeassistant/components/upnp/.translations/et.json b/homeassistant/components/upnp/.translations/et.json new file mode 100644 index 00000000000..0c49a92bc0a --- /dev/null +++ b/homeassistant/components/upnp/.translations/et.json @@ -0,0 +1,15 @@ +{ + "config": { + "step": { + "init": { + "title": "" + }, + "user": { + "data": { + "igd": "" + } + } + }, + "title": "" + } +} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/et.json b/homeassistant/components/zone/.translations/et.json new file mode 100644 index 00000000000..aa921f376e7 --- /dev/null +++ b/homeassistant/components/zone/.translations/et.json @@ -0,0 +1,16 @@ +{ + "config": { + "step": { + "init": { + "data": { + "icon": "Ikoon", + "latitude": "Laius", + "longitude": "Pikkus", + "name": "Nimi", + "radius": "Raadius" + }, + "title": "M\u00e4\u00e4ra tsooni parameetrid" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zwave/.translations/et.json b/homeassistant/components/zwave/.translations/et.json new file mode 100644 index 00000000000..8c4c45f9c89 --- /dev/null +++ b/homeassistant/components/zwave/.translations/et.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "" + } +} \ No newline at end of file From 798f63002953074c39e445411ea057f036c44146 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 13 Jan 2019 09:38:35 -0800 Subject: [PATCH 149/235] Updated frontend to 20190113.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 026311f1397..03b984eeaef 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -24,7 +24,7 @@ from homeassistant.core import callback from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass -REQUIREMENTS = ['home-assistant-frontend==20190109.1'] +REQUIREMENTS = ['home-assistant-frontend==20190113.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log', diff --git a/requirements_all.txt b/requirements_all.txt index a99eadc4473..22cc8e6ea97 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -514,7 +514,7 @@ hole==0.3.0 holidays==0.9.9 # homeassistant.components.frontend -home-assistant-frontend==20190109.1 +home-assistant-frontend==20190113.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 37670a72909..bdc9268f9ba 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -107,7 +107,7 @@ hdate==0.8.7 holidays==0.9.9 # homeassistant.components.frontend -home-assistant-frontend==20190109.1 +home-assistant-frontend==20190113.0 # homeassistant.components.homematicip_cloud homematicip==0.10.3 From db8784233515f234e46ed7ee0e96509b09d94b31 Mon Sep 17 00:00:00 2001 From: Matt Snyder Date: Sun, 13 Jan 2019 11:47:50 -0600 Subject: [PATCH 150/235] Show persistent notification on Doorbird schedule failure (#20033) * Remove unnecessary return. Add persistent notification on failure to configure doorbird schedule. * Update doorbirdpy to 2.0.5 * Fix bare except * Bump version again * Lint * Return false --- homeassistant/components/doorbird.py | 19 +++++++++++++++---- requirements_all.txt | 2 +- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/doorbird.py b/homeassistant/components/doorbird.py index f952d1ddc1c..28747bbe8be 100644 --- a/homeassistant/components/doorbird.py +++ b/homeassistant/components/doorbird.py @@ -6,6 +6,7 @@ https://home-assistant.io/components/doorbird/ """ import logging +from urllib.error import HTTPError import voluptuous as vol from homeassistant.components.http import HomeAssistantView @@ -14,7 +15,7 @@ from homeassistant.const import CONF_HOST, CONF_USERNAME, \ import homeassistant.helpers.config_validation as cv from homeassistant.util import slugify, dt as dt_util -REQUIREMENTS = ['doorbirdpy==2.0.4'] +REQUIREMENTS = ['doorbirdpy==2.0.6'] _LOGGER = logging.getLogger(__name__) @@ -113,7 +114,18 @@ def setup(hass, config): # Subscribe to doorbell or motion events if events: - doorstation.update_schedule(hass) + try: + doorstation.update_schedule(hass) + except HTTPError: + hass.components.persistent_notification.create( + 'Doorbird configuration failed. Please verify that API ' + 'Operator permission is enabled for the Doorbird user. ' + 'A restart will be required once permissions have been ' + 'verified.', + title='Doorbird Configuration Failure', + notification_id='doorbird_schedule_error') + + return False hass.data[DOMAIN] = doorstations @@ -230,6 +242,7 @@ class ConfiguredDoorBird(): if not self.webhook_is_registered(hass_url): self.device.change_favorite('http', 'Home Assistant ({} events)' .format(event), hass_url) + fav_id = self.get_webhook_id(hass_url) if not fav_id: @@ -253,8 +266,6 @@ class ConfiguredDoorBird(): for relay in self._relay_nums: entry = self.device.get_schedule_entry(event, str(relay)) entry.output.append(output) - resp = self.device.change_schedule(entry) - return resp else: entry = self.device.get_schedule_entry(event) entry.output.append(output) diff --git a/requirements_all.txt b/requirements_all.txt index 22cc8e6ea97..f7f56db88b7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -327,7 +327,7 @@ distro==1.3.0 dlipower==0.7.165 # homeassistant.components.doorbird -doorbirdpy==2.0.4 +doorbirdpy==2.0.6 # homeassistant.components.sensor.dovado dovado==0.4.1 From 3b83a64f7cdf8a3b90f7f445869155c549c631b0 Mon Sep 17 00:00:00 2001 From: Austin Drummond Date: Sun, 13 Jan 2019 13:09:47 -0500 Subject: [PATCH 151/235] Add support for HomeKit Controller covers (#19866) * Added support for HomeKit Controller covers * removed copied code * more linting fixes * added device type to service info * added checks for value in characteristics * added state stopped parsing * removed logger * removed unused args * fixed inits * removed unused imports * fixed lint issues * fixed lint issues * remove state_unknown * remove validation of kwargs in homekit controller covers * guarantee tilt position is not none before setting --- .../components/cover/homekit_controller.py | 305 ++++++++++++++++++ .../components/homekit_controller/__init__.py | 12 +- 2 files changed, 313 insertions(+), 4 deletions(-) create mode 100644 homeassistant/components/cover/homekit_controller.py diff --git a/homeassistant/components/cover/homekit_controller.py b/homeassistant/components/cover/homekit_controller.py new file mode 100644 index 00000000000..cd3bc511291 --- /dev/null +++ b/homeassistant/components/cover/homekit_controller.py @@ -0,0 +1,305 @@ +""" +Support for Homekit Cover. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/cover.homekit_controller/ +""" +import logging + +from homeassistant.components.homekit_controller import (HomeKitEntity, + KNOWN_ACCESSORIES) +from homeassistant.components.cover import ( + CoverDevice, SUPPORT_OPEN, SUPPORT_CLOSE, SUPPORT_SET_POSITION, + SUPPORT_OPEN_TILT, SUPPORT_CLOSE_TILT, SUPPORT_SET_TILT_POSITION, + ATTR_POSITION, ATTR_TILT_POSITION) +from homeassistant.const import ( + STATE_CLOSED, STATE_CLOSING, STATE_OPEN, STATE_OPENING) + +STATE_STOPPED = 'stopped' + +DEPENDENCIES = ['homekit_controller'] + +_LOGGER = logging.getLogger(__name__) + +CURRENT_GARAGE_STATE_MAP = { + 0: STATE_OPEN, + 1: STATE_CLOSED, + 2: STATE_OPENING, + 3: STATE_CLOSING, + 4: STATE_STOPPED +} + +TARGET_GARAGE_STATE_MAP = { + STATE_OPEN: 0, + STATE_CLOSED: 1, + STATE_STOPPED: 2 +} + +CURRENT_WINDOW_STATE_MAP = { + 0: STATE_OPENING, + 1: STATE_CLOSING, + 2: STATE_STOPPED +} + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up HomeKit Cover support.""" + if discovery_info is None: + return + accessory = hass.data[KNOWN_ACCESSORIES][discovery_info['serial']] + + if discovery_info['device-type'] == 'garage-door-opener': + add_entities([HomeKitGarageDoorCover(accessory, discovery_info)], + True) + else: + add_entities([HomeKitWindowCover(accessory, discovery_info)], + True) + + +class HomeKitGarageDoorCover(HomeKitEntity, CoverDevice): + """Representation of a HomeKit Garage Door.""" + + def __init__(self, accessory, discovery_info): + """Initialise the Cover.""" + super().__init__(accessory, discovery_info) + self._name = None + self._state = None + self._obstruction_detected = None + self.lock_state = None + + @property + def device_class(self): + """Define this cover as a garage door.""" + return 'garage' + + def update_characteristics(self, characteristics): + """Synchronise the Cover state with Home Assistant.""" + # pylint: disable=import-error + from homekit.model.characteristics import CharacteristicsTypes + + for characteristic in characteristics: + ctype = characteristic['type'] + ctype = CharacteristicsTypes.get_short(ctype) + if ctype == "door-state.current": + self._chars['door-state.current'] = \ + characteristic['iid'] + self._state = CURRENT_GARAGE_STATE_MAP[characteristic['value']] + elif ctype == "door-state.target": + self._chars['door-state.target'] = \ + characteristic['iid'] + elif ctype == "obstruction-detected": + self._chars['obstruction-detected'] = characteristic['iid'] + self._obstruction_detected = characteristic['value'] + elif ctype == "name": + self._chars['name'] = characteristic['iid'] + self._name = characteristic['value'] + + @property + def name(self): + """Return the name of the cover.""" + return self._name + + @property + def available(self): + """Return True if entity is available.""" + return self._state is not None + + @property + def supported_features(self): + """Flag supported features.""" + return SUPPORT_OPEN | SUPPORT_CLOSE + + @property + def is_closed(self): + """Return true if cover is closed, else False.""" + return self._state == STATE_CLOSED + + @property + def is_closing(self): + """Return if the cover is closing or not.""" + return self._state == STATE_CLOSING + + @property + def is_opening(self): + """Return if the cover is opening or not.""" + return self._state == STATE_OPENING + + def open_cover(self, **kwargs): + """Send open command.""" + self.set_door_state(STATE_OPEN) + + def close_cover(self, **kwargs): + """Send close command.""" + self.set_door_state(STATE_CLOSED) + + def set_door_state(self, state): + """Send state command.""" + characteristics = [{'aid': self._aid, + 'iid': self._chars['door-state.target'], + 'value': TARGET_GARAGE_STATE_MAP[state]}] + self.put_characteristics(characteristics) + + @property + def device_state_attributes(self): + """Return the optional state attributes.""" + if self._obstruction_detected is None: + return None + + return { + 'obstruction-detected': self._obstruction_detected, + } + + +class HomeKitWindowCover(HomeKitEntity, CoverDevice): + """Representation of a HomeKit Window or Window Covering.""" + + def __init__(self, accessory, discovery_info): + """Initialise the Cover.""" + super().__init__(accessory, discovery_info) + self._name = None + self._state = None + self._position = None + self._tilt_position = None + self._hold = None + self._obstruction_detected = None + self.lock_state = None + + @property + def available(self): + """Return True if entity is available.""" + return self._state is not None + + def update_characteristics(self, characteristics): + """Synchronise the Cover state with Home Assistant.""" + # pylint: disable=import-error + from homekit.model.characteristics import CharacteristicsTypes + + for characteristic in characteristics: + ctype = characteristic['type'] + ctype = CharacteristicsTypes.get_short(ctype) + if ctype == "position.state": + self._chars['position.state'] = \ + characteristic['iid'] + if 'value' in characteristic: + self._state = \ + CURRENT_WINDOW_STATE_MAP[characteristic['value']] + elif ctype == "position.current": + self._chars['position.current'] = \ + characteristic['iid'] + self._position = characteristic['value'] + elif ctype == "position.target": + self._chars['position.target'] = \ + characteristic['iid'] + elif ctype == "position.hold": + self._chars['position.hold'] = characteristic['iid'] + if 'value' in characteristic: + self._hold = characteristic['value'] + elif ctype == "vertical-tilt.current": + self._chars['vertical-tilt.current'] = characteristic['iid'] + if characteristic['value'] is not None: + self._tilt_position = characteristic['value'] + elif ctype == "horizontal-tilt.current": + self._chars['horizontal-tilt.current'] = characteristic['iid'] + if characteristic['value'] is not None: + self._tilt_position = characteristic['value'] + elif ctype == "vertical-tilt.target": + self._chars['vertical-tilt.target'] = \ + characteristic['iid'] + elif ctype == "horizontal-tilt.target": + self._chars['vertical-tilt.target'] = \ + characteristic['iid'] + elif ctype == "obstruction-detected": + self._chars['obstruction-detected'] = characteristic['iid'] + self._obstruction_detected = characteristic['value'] + elif ctype == "name": + self._chars['name'] = characteristic['iid'] + if 'value' in characteristic: + self._name = characteristic['value'] + + @property + def name(self): + """Return the name of the cover.""" + return self._name + + @property + def supported_features(self): + """Flag supported features.""" + supported_features = ( + SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_SET_POSITION) + + if self._tilt_position is not None: + supported_features |= ( + SUPPORT_OPEN_TILT | SUPPORT_CLOSE_TILT | + SUPPORT_SET_TILT_POSITION) + + return supported_features + + @property + def current_cover_position(self): + """Return the current position of cover.""" + return self._position + + @property + def is_closed(self): + """Return true if cover is closed, else False.""" + return self._position == 0 + + @property + def is_closing(self): + """Return if the cover is closing or not.""" + return self._state == STATE_CLOSING + + @property + def is_opening(self): + """Return if the cover is opening or not.""" + return self._state == STATE_OPENING + + def open_cover(self, **kwargs): + """Send open command.""" + self.set_cover_position(position=100) + + def close_cover(self, **kwargs): + """Send close command.""" + self.set_cover_position(position=0) + + def set_cover_position(self, **kwargs): + """Send position command.""" + position = kwargs[ATTR_POSITION] + characteristics = [{'aid': self._aid, + 'iid': self._chars['position.target'], + 'value': position}] + self.put_characteristics(characteristics) + + @property + def current_cover_tilt_position(self): + """Return current position of cover tilt.""" + return self._tilt_position + + def set_cover_tilt_position(self, **kwargs): + """Move the cover tilt to a specific position.""" + tilt_position = kwargs[ATTR_TILT_POSITION] + if 'vertical-tilt.target' in self._chars: + characteristics = [{'aid': self._aid, + 'iid': self._chars['vertical-tilt.target'], + 'value': tilt_position}] + self.put_characteristics(characteristics) + elif 'horizontal-tilt.target' in self._chars: + characteristics = [{'aid': self._aid, + 'iid': + self._chars['horizontal-tilt.target'], + 'value': tilt_position}] + self.put_characteristics(characteristics) + + @property + def device_state_attributes(self): + """Return the optional state attributes.""" + state_attributes = {} + if self._obstruction_detected is not None: + state_attributes['obstruction-detected'] = \ + self._obstruction_detected + + if self._hold is not None: + state_attributes['hold-position'] = \ + self._hold + + return state_attributes diff --git a/homeassistant/components/homekit_controller/__init__.py b/homeassistant/components/homekit_controller/__init__.py index a8264e6a080..6fdde7ddd50 100644 --- a/homeassistant/components/homekit_controller/__init__.py +++ b/homeassistant/components/homekit_controller/__init__.py @@ -25,6 +25,9 @@ HOMEKIT_ACCESSORY_DISPATCH = { 'switch': 'switch', 'thermostat': 'climate', 'security-system': 'alarm_control_panel', + 'garage-door-opener': 'cover', + 'window': 'cover', + 'window-covering': 'cover', 'lock-mechanism': 'lock' } @@ -115,12 +118,13 @@ class HKDevice(): self.hass.data[KNOWN_ACCESSORIES][serial] = self aid = accessory['aid'] for service in accessory['services']: - service_info = {'serial': serial, - 'aid': aid, - 'model': self.model, - 'iid': service['iid']} devtype = ServicesTypes.get_short(service['type']) _LOGGER.debug("Found %s", devtype) + service_info = {'serial': serial, + 'aid': aid, + 'iid': service['iid'], + 'model': self.model, + 'device-type': devtype} component = HOMEKIT_ACCESSORY_DISPATCH.get(devtype, None) if component is not None: discovery.load_platform(self.hass, component, DOMAIN, From 0f92d061c4d65ee9611b9a062f6e1011e493f53c Mon Sep 17 00:00:00 2001 From: Rohan Kapoor Date: Sun, 13 Jan 2019 11:49:20 -0800 Subject: [PATCH 152/235] Use voluptuous to validate the webhook schema --- homeassistant/components/locative/__init__.py | 44 ++++++++++--------- 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/locative/__init__.py b/homeassistant/components/locative/__init__.py index f33974e2827..d032af0de5b 100644 --- a/homeassistant/components/locative/__init__.py +++ b/homeassistant/components/locative/__init__.py @@ -6,10 +6,13 @@ https://home-assistant.io/components/locative/ """ import logging +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv from homeassistant.components.device_tracker import \ DOMAIN as DEVICE_TRACKER_DOMAIN from homeassistant.const import HTTP_UNPROCESSABLE_ENTITY, ATTR_LATITUDE, \ - ATTR_LONGITUDE, STATE_NOT_HOME, CONF_WEBHOOK_ID + ATTR_LONGITUDE, STATE_NOT_HOME, CONF_WEBHOOK_ID, ATTR_ID from homeassistant.helpers import config_entry_flow from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -22,6 +25,18 @@ DEPENDENCIES = ['webhook'] TRACKER_UPDATE = '{}_tracker_update'.format(DOMAIN) +ATTR_DEVICE_ID = 'device' +ATTR_TRIGGER = 'trigger' + +WEBHOOK_SCHEMA = vol.Schema({ + vol.Required(ATTR_LATITUDE): cv.latitude, + vol.Required(ATTR_LONGITUDE): cv.longitude, + vol.Required(ATTR_DEVICE_ID): cv.string, + vol.Required(ATTR_TRIGGER): cv.string, + vol.Optional(ATTR_ID): cv.string, +}, extra=vol.ALLOW_EXTRA) + + async def async_setup(hass, hass_config): """Set up the Locative component.""" hass.async_create_task( @@ -32,30 +47,19 @@ async def async_setup(hass, hass_config): async def handle_webhook(hass, webhook_id, request): """Handle incoming webhook from Locative.""" - data = await request.post() + try: + data = WEBHOOK_SCHEMA(dict(await request.post())) + except vol.MultipleInvalid as e: + return e.error_message, HTTP_UNPROCESSABLE_ENTITY - if 'latitude' not in data or 'longitude' not in data: - return ('Latitude and longitude not specified.', - HTTP_UNPROCESSABLE_ENTITY) - - if 'device' not in data: - _LOGGER.error('Device id not specified.') - return ('Device id not specified.', - HTTP_UNPROCESSABLE_ENTITY) - - if 'trigger' not in data: - _LOGGER.error('Trigger is not specified.') - return ('Trigger is not specified.', - HTTP_UNPROCESSABLE_ENTITY) - - if 'id' not in data and data['trigger'] != 'test': + if ATTR_ID not in data and data[ATTR_TRIGGER] != 'test': _LOGGER.error('Location id not specified.') return ('Location id not specified.', HTTP_UNPROCESSABLE_ENTITY) - device = data['device'].replace('-', '') - location_name = data.get('id', data['trigger']).lower() - direction = data['trigger'] + device = data[ATTR_DEVICE_ID].replace('-', '') + location_name = data.get(ATTR_ID, data[ATTR_TRIGGER]).lower() + direction = data[ATTR_TRIGGER] gps_location = (data[ATTR_LATITUDE], data[ATTR_LONGITUDE]) if direction == 'enter': From b75356d5325ce5b915f7bdb72c46fda53f190865 Mon Sep 17 00:00:00 2001 From: Rohan Kapoor Date: Sun, 13 Jan 2019 12:12:04 -0800 Subject: [PATCH 153/235] Validate test mode schema as well --- homeassistant/components/locative/__init__.py | 39 ++++++++++++------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/locative/__init__.py b/homeassistant/components/locative/__init__.py index d032af0de5b..daa8cb2295b 100644 --- a/homeassistant/components/locative/__init__.py +++ b/homeassistant/components/locative/__init__.py @@ -7,6 +7,7 @@ https://home-assistant.io/components/locative/ import logging import voluptuous as vol +from typing import Dict import homeassistant.helpers.config_validation as cv from homeassistant.components.device_tracker import \ @@ -28,13 +29,30 @@ TRACKER_UPDATE = '{}_tracker_update'.format(DOMAIN) ATTR_DEVICE_ID = 'device' ATTR_TRIGGER = 'trigger' -WEBHOOK_SCHEMA = vol.Schema({ - vol.Required(ATTR_LATITUDE): cv.latitude, - vol.Required(ATTR_LONGITUDE): cv.longitude, - vol.Required(ATTR_DEVICE_ID): cv.string, - vol.Required(ATTR_TRIGGER): cv.string, - vol.Optional(ATTR_ID): cv.string, -}, extra=vol.ALLOW_EXTRA) + +def _id(value: str) -> str: + """Coerce id by removing '-'.""" + return value.replace('-', '') + + +def _validate_test_mode(obj: Dict) -> Dict: + """Validate that id is provided outside of test mode.""" + if ATTR_ID not in obj and obj[ATTR_TRIGGER] != 'test': + raise vol.Invalid('Location id not specified') + return obj + + +WEBHOOK_SCHEMA = vol.All( + dict, + vol.Schema({ + vol.Required(ATTR_LATITUDE): cv.latitude, + vol.Required(ATTR_LONGITUDE): cv.longitude, + vol.Required(ATTR_DEVICE_ID): cv.string, + vol.Required(ATTR_TRIGGER): cv.string, + vol.Optional(ATTR_ID): vol.All(cv.string, _id) + }, extra=vol.ALLOW_EXTRA), + _validate_test_mode +) async def async_setup(hass, hass_config): @@ -52,12 +70,7 @@ async def handle_webhook(hass, webhook_id, request): except vol.MultipleInvalid as e: return e.error_message, HTTP_UNPROCESSABLE_ENTITY - if ATTR_ID not in data and data[ATTR_TRIGGER] != 'test': - _LOGGER.error('Location id not specified.') - return ('Location id not specified.', - HTTP_UNPROCESSABLE_ENTITY) - - device = data[ATTR_DEVICE_ID].replace('-', '') + device = data[ATTR_DEVICE_ID] location_name = data.get(ATTR_ID, data[ATTR_TRIGGER]).lower() direction = data[ATTR_TRIGGER] gps_location = (data[ATTR_LATITUDE], data[ATTR_LONGITUDE]) From 2a2318b7f65a9f709943f0b18bec817c4784ee45 Mon Sep 17 00:00:00 2001 From: Teemu R Date: Sun, 13 Jan 2019 21:31:08 +0100 Subject: [PATCH 154/235] warning -> debug, this should not have been visible to users (#20061) --- 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 c79470a82fe..165ef668a95 100644 --- a/homeassistant/components/media_player/songpal.py +++ b/homeassistant/components/media_player/songpal.py @@ -154,8 +154,8 @@ class SongpalDevice(MediaPlayerDevice): _LOGGER.debug("New active source: %s", self._active_source) await self.async_update_ha_state() else: - _LOGGER.warning("Got non-handled content change: %s", - content) + _LOGGER.debug("Got non-handled content change: %s", + content) async def _power_changed(power: PowerChange): _LOGGER.debug("Power changed: %s", power) From 9036aafc817673a37bf4d4bc85a5050b45db436b Mon Sep 17 00:00:00 2001 From: Rohan Kapoor Date: Sun, 13 Jan 2019 13:26:51 -0800 Subject: [PATCH 155/235] Lint --- homeassistant/components/locative/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/locative/__init__.py b/homeassistant/components/locative/__init__.py index daa8cb2295b..025f57f5963 100644 --- a/homeassistant/components/locative/__init__.py +++ b/homeassistant/components/locative/__init__.py @@ -5,9 +5,9 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/locative/ """ import logging +from typing import Dict import voluptuous as vol -from typing import Dict import homeassistant.helpers.config_validation as cv from homeassistant.components.device_tracker import \ @@ -67,8 +67,8 @@ async def handle_webhook(hass, webhook_id, request): """Handle incoming webhook from Locative.""" try: data = WEBHOOK_SCHEMA(dict(await request.post())) - except vol.MultipleInvalid as e: - return e.error_message, HTTP_UNPROCESSABLE_ENTITY + except vol.MultipleInvalid as error: + return error.error_message, HTTP_UNPROCESSABLE_ENTITY device = data[ATTR_DEVICE_ID] location_name = data.get(ATTR_ID, data[ATTR_TRIGGER]).lower() From e476949c3e8527ad3f6d9d4436bd8a41523c0477 Mon Sep 17 00:00:00 2001 From: Rohan Kapoor Date: Sun, 13 Jan 2019 13:35:13 -0800 Subject: [PATCH 156/235] Remove allow_extra --- homeassistant/components/locative/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/locative/__init__.py b/homeassistant/components/locative/__init__.py index 025f57f5963..025b14205b4 100644 --- a/homeassistant/components/locative/__init__.py +++ b/homeassistant/components/locative/__init__.py @@ -50,7 +50,7 @@ WEBHOOK_SCHEMA = vol.All( vol.Required(ATTR_DEVICE_ID): cv.string, vol.Required(ATTR_TRIGGER): cv.string, vol.Optional(ATTR_ID): vol.All(cv.string, _id) - }, extra=vol.ALLOW_EXTRA), + }), _validate_test_mode ) From 7f3871028d7c5d260fbd4d720948e30cb3121c22 Mon Sep 17 00:00:00 2001 From: Rohan Kapoor Date: Sun, 13 Jan 2019 16:09:47 -0800 Subject: [PATCH 157/235] Split out gpslogger into a separate component and platform (#20044) * Split out gpslogger into a separate component and platform * Lint * Lint * Increase test coverage --- .coveragerc | 1 - .../components/device_tracker/gpslogger.py | 100 ++------- .../components/gpslogger/__init__.py | 114 +++++++++++ tests/components/gpslogger/__init__.py | 1 + tests/components/gpslogger/test_init.py | 189 ++++++++++++++++++ 5 files changed, 316 insertions(+), 89 deletions(-) create mode 100644 homeassistant/components/gpslogger/__init__.py create mode 100644 tests/components/gpslogger/__init__.py create mode 100644 tests/components/gpslogger/test_init.py diff --git a/.coveragerc b/.coveragerc index 568dfd79386..aedf311d6cd 100644 --- a/.coveragerc +++ b/.coveragerc @@ -525,7 +525,6 @@ omit = homeassistant/components/device_tracker/fritz.py homeassistant/components/device_tracker/google_maps.py homeassistant/components/device_tracker/googlehome.py - homeassistant/components/device_tracker/gpslogger.py homeassistant/components/device_tracker/hitron_coda.py homeassistant/components/device_tracker/huawei_router.py homeassistant/components/device_tracker/icloud.py diff --git a/homeassistant/components/device_tracker/gpslogger.py b/homeassistant/components/device_tracker/gpslogger.py index f39684aa834..e0d9b37bf84 100644 --- a/homeassistant/components/device_tracker/gpslogger.py +++ b/homeassistant/components/device_tracker/gpslogger.py @@ -5,104 +5,28 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/device_tracker.gpslogger/ """ import logging -from hmac import compare_digest -from aiohttp.web import Request, HTTPUnauthorized -import voluptuous as vol - -import homeassistant.helpers.config_validation as cv -from homeassistant.const import ( - CONF_PASSWORD, HTTP_UNPROCESSABLE_ENTITY -) -from homeassistant.components.http import ( - CONF_API_PASSWORD, HomeAssistantView -) -# pylint: disable=unused-import -from homeassistant.components.device_tracker import ( # NOQA - DOMAIN, PLATFORM_SCHEMA -) +from homeassistant.components.gpslogger import TRACKER_UPDATE +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.typing import HomeAssistantType, ConfigType _LOGGER = logging.getLogger(__name__) -DEPENDENCIES = ['http'] - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_PASSWORD): cv.string, -}) +DEPENDENCIES = ['gpslogger'] async def async_setup_scanner(hass: HomeAssistantType, config: ConfigType, async_see, discovery_info=None): - """Set up an endpoint for the GPSLogger application.""" - hass.http.register_view(GPSLoggerView(async_see, config)) - - return True - - -class GPSLoggerView(HomeAssistantView): - """View to handle GPSLogger requests.""" - - url = '/api/gpslogger' - name = 'api:gpslogger' - - def __init__(self, async_see, config): - """Initialize GPSLogger url endpoints.""" - self.async_see = async_see - self._password = config.get(CONF_PASSWORD) - # this component does not require external authentication if - # password is set - self.requires_auth = self._password is None - - async def get(self, request: Request): - """Handle for GPSLogger message received as GET.""" - hass = request.app['hass'] - data = request.query - - if self._password is not None: - authenticated = CONF_API_PASSWORD in data and compare_digest( - self._password, - data[CONF_API_PASSWORD] - ) - if not authenticated: - raise HTTPUnauthorized() - - if 'latitude' not in data or 'longitude' not in data: - return ('Latitude and longitude not specified.', - HTTP_UNPROCESSABLE_ENTITY) - - if 'device' not in data: - _LOGGER.error("Device id not specified") - return ('Device id not specified.', - HTTP_UNPROCESSABLE_ENTITY) - - device = data['device'].replace('-', '') - gps_location = (data['latitude'], data['longitude']) - accuracy = 200 - battery = -1 - - if 'accuracy' in data: - accuracy = int(float(data['accuracy'])) - if 'battery' in data: - battery = float(data['battery']) - - attrs = {} - if 'speed' in data: - attrs['speed'] = float(data['speed']) - if 'direction' in data: - attrs['direction'] = float(data['direction']) - if 'altitude' in data: - attrs['altitude'] = float(data['altitude']) - if 'provider' in data: - attrs['provider'] = data['provider'] - if 'activity' in data: - attrs['activity'] = data['activity'] - - hass.async_create_task(self.async_see( + """Set up an endpoint for the GPSLogger device tracker.""" + async def _set_location(device, gps_location, battery, accuracy, attrs): + """Fire HA event to set location.""" + await async_see( dev_id=device, - gps=gps_location, battery=battery, + gps=gps_location, + battery=battery, gps_accuracy=accuracy, attributes=attrs - )) + ) - return 'Setting location for {}'.format(device) + async_dispatcher_connect(hass, TRACKER_UPDATE, _set_location) + return True diff --git a/homeassistant/components/gpslogger/__init__.py b/homeassistant/components/gpslogger/__init__.py new file mode 100644 index 00000000000..e978e3706be --- /dev/null +++ b/homeassistant/components/gpslogger/__init__.py @@ -0,0 +1,114 @@ +""" +Support for GPSLogger. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/gpslogger/ +""" +import logging +from hmac import compare_digest + +import voluptuous as vol +from aiohttp.web_exceptions import HTTPUnauthorized +from aiohttp.web_request import Request + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.http import HomeAssistantView, CONF_API_PASSWORD +from homeassistant.const import CONF_PASSWORD, HTTP_UNPROCESSABLE_ENTITY +from homeassistant.helpers.discovery import async_load_platform +from homeassistant.helpers.dispatcher import async_dispatcher_send + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = 'gpslogger' +DEPENDENCIES = ['http'] + +CONFIG_SCHEMA = vol.Schema({ + vol.Optional(DOMAIN): vol.Schema({ + vol.Optional(CONF_PASSWORD): cv.string, + }), +}, extra=vol.ALLOW_EXTRA) + +URL = '/api/{}'.format(DOMAIN) + +TRACKER_UPDATE = '{}_tracker_update'.format(DOMAIN) + + +async def async_setup(hass, hass_config): + """Set up the GPSLogger component.""" + config = hass_config[DOMAIN] + hass.http.register_view(GPSLoggerView(config)) + + hass.async_create_task( + async_load_platform(hass, 'device_tracker', DOMAIN, {}, hass_config) + ) + return True + + +class GPSLoggerView(HomeAssistantView): + """View to handle GPSLogger requests.""" + + url = URL + name = 'api:gpslogger' + + def __init__(self, config): + """Initialize GPSLogger url endpoints.""" + self._password = config.get(CONF_PASSWORD) + # this component does not require external authentication if + # password is set + self.requires_auth = self._password is None + + async def get(self, request: Request): + """Handle for GPSLogger message received as GET.""" + hass = request.app['hass'] + data = request.query + + if self._password is not None: + authenticated = CONF_API_PASSWORD in data and compare_digest( + self._password, + data[CONF_API_PASSWORD] + ) + if not authenticated: + raise HTTPUnauthorized() + + if 'latitude' not in data or 'longitude' not in data: + return ('Latitude and longitude not specified.', + HTTP_UNPROCESSABLE_ENTITY) + + if 'device' not in data: + _LOGGER.error("Device id not specified") + return ('Device id not specified.', + HTTP_UNPROCESSABLE_ENTITY) + + device = data['device'].replace('-', '') + gps_location = (data['latitude'], data['longitude']) + accuracy = 200 + battery = -1 + + if 'accuracy' in data: + accuracy = int(float(data['accuracy'])) + if 'battery' in data: + battery = float(data['battery']) + + attrs = {} + if 'speed' in data: + attrs['speed'] = float(data['speed']) + if 'direction' in data: + attrs['direction'] = float(data['direction']) + if 'altitude' in data: + attrs['altitude'] = float(data['altitude']) + if 'provider' in data: + attrs['provider'] = data['provider'] + if 'activity' in data: + attrs['activity'] = data['activity'] + + async_dispatcher_send( + hass, + TRACKER_UPDATE, + device, + gps_location, + battery, + accuracy, + attrs + ) + + return 'Setting location for {}'.format(device) diff --git a/tests/components/gpslogger/__init__.py b/tests/components/gpslogger/__init__.py new file mode 100644 index 00000000000..636a9a767f9 --- /dev/null +++ b/tests/components/gpslogger/__init__.py @@ -0,0 +1 @@ +"""Tests for the GPSLogger component.""" diff --git a/tests/components/gpslogger/test_init.py b/tests/components/gpslogger/test_init.py new file mode 100644 index 00000000000..539b9d549d3 --- /dev/null +++ b/tests/components/gpslogger/test_init.py @@ -0,0 +1,189 @@ +"""The tests the for GPSLogger device tracker platform.""" +from unittest.mock import patch + +import pytest + +from homeassistant.components import zone +from homeassistant.components.device_tracker import \ + DOMAIN as DEVICE_TRACKER_DOMAIN +from homeassistant.components.gpslogger import URL, DOMAIN +from homeassistant.components.http import CONF_API_PASSWORD +from homeassistant.const import HTTP_OK, HTTP_UNPROCESSABLE_ENTITY, \ + STATE_HOME, STATE_NOT_HOME, HTTP_UNAUTHORIZED, CONF_PASSWORD +from homeassistant.setup import async_setup_component + +HOME_LATITUDE = 37.239622 +HOME_LONGITUDE = -115.815811 + + +def _url(data=None): + """Generate URL.""" + data = data or {} + data = "&".join(["{}={}".format(name, value) for + name, value in data.items()]) + return "{}?{}".format(URL, data) + + +@pytest.fixture(autouse=True) +def mock_dev_track(mock_device_tracker_conf): + """Mock device tracker config loading.""" + pass + + +@pytest.fixture +def authenticated_gpslogger_client(loop, hass, hass_client): + """Locative mock client (authenticated).""" + assert loop.run_until_complete(async_setup_component( + hass, DOMAIN, { + DOMAIN: {} + })) + + with patch('homeassistant.components.device_tracker.update_config'): + yield loop.run_until_complete(hass_client()) + + +@pytest.fixture +def unauthenticated_gpslogger_client(loop, hass, aiohttp_client): + """Locative mock client (unauthenticated).""" + assert loop.run_until_complete(async_setup_component( + hass, 'persistent_notification', {})) + + assert loop.run_until_complete(async_setup_component( + hass, DOMAIN, { + DOMAIN: { + CONF_PASSWORD: 'test' + } + })) + + with patch('homeassistant.components.device_tracker.update_config'): + yield loop.run_until_complete(aiohttp_client(hass.http.app)) + + +@pytest.fixture(autouse=True) +def setup_zones(loop, hass): + """Set up Zone config in HA.""" + assert loop.run_until_complete(async_setup_component( + hass, zone.DOMAIN, { + 'zone': { + 'name': 'Home', + 'latitude': HOME_LATITUDE, + 'longitude': HOME_LONGITUDE, + 'radius': 100, + }})) + + +async def test_authentication(hass, unauthenticated_gpslogger_client): + """Test missing data.""" + data = { + 'latitude': 1.0, + 'longitude': 1.1, + 'device': '123', + CONF_API_PASSWORD: 'test' + } + + # No auth + req = await unauthenticated_gpslogger_client.get(_url({})) + await hass.async_block_till_done() + assert req.status == HTTP_UNAUTHORIZED + + # Authenticated + req = await unauthenticated_gpslogger_client.get(_url(data)) + await hass.async_block_till_done() + assert req.status == HTTP_OK + state_name = hass.states.get('{}.{}'.format(DEVICE_TRACKER_DOMAIN, + data['device'])).state + assert STATE_NOT_HOME == state_name + + +async def test_missing_data(hass, authenticated_gpslogger_client): + """Test missing data.""" + data = { + 'latitude': 1.0, + 'longitude': 1.1, + 'device': '123', + } + + # No data + req = await authenticated_gpslogger_client.get(_url({})) + await hass.async_block_till_done() + assert req.status == HTTP_UNPROCESSABLE_ENTITY + + # No latitude + copy = data.copy() + del copy['latitude'] + req = await authenticated_gpslogger_client.get(_url(copy)) + await hass.async_block_till_done() + assert req.status == HTTP_UNPROCESSABLE_ENTITY + + # No device + copy = data.copy() + del copy['device'] + req = await authenticated_gpslogger_client.get(_url(copy)) + await hass.async_block_till_done() + assert req.status == HTTP_UNPROCESSABLE_ENTITY + + +async def test_enter_and_exit(hass, authenticated_gpslogger_client): + """Test when there is a known zone.""" + data = { + 'latitude': HOME_LATITUDE, + 'longitude': HOME_LONGITUDE, + 'device': '123', + } + + # Enter the Home + req = await authenticated_gpslogger_client.get(_url(data)) + await hass.async_block_till_done() + assert req.status == HTTP_OK + state_name = hass.states.get('{}.{}'.format(DEVICE_TRACKER_DOMAIN, + data['device'])).state + assert STATE_HOME == state_name + + # Enter Home again + req = await authenticated_gpslogger_client.get(_url(data)) + await hass.async_block_till_done() + assert req.status == HTTP_OK + state_name = hass.states.get('{}.{}'.format(DEVICE_TRACKER_DOMAIN, + data['device'])).state + assert STATE_HOME == state_name + + data['longitude'] = 0 + data['latitude'] = 0 + + # Enter Somewhere else + req = await authenticated_gpslogger_client.get(_url(data)) + await hass.async_block_till_done() + assert req.status == HTTP_OK + state_name = hass.states.get('{}.{}'.format(DEVICE_TRACKER_DOMAIN, + data['device'])).state + assert STATE_NOT_HOME == state_name + + +async def test_enter_with_attrs(hass, authenticated_gpslogger_client): + """Test when additional attributes are present.""" + data = { + 'latitude': 1.0, + 'longitude': 1.1, + 'device': '123', + 'accuracy': 10.5, + 'battery': 10, + 'speed': 100, + 'direction': 105.32, + 'altitude': 102, + 'provider': 'gps', + 'activity': 'running' + } + + req = await authenticated_gpslogger_client.get(_url(data)) + await hass.async_block_till_done() + assert req.status == HTTP_OK + state = hass.states.get('{}.{}'.format(DEVICE_TRACKER_DOMAIN, + data['device'])) + assert STATE_NOT_HOME == state.state + assert 10 == state.attributes['gps_accuracy'] + assert 10.0 == state.attributes['battery'] + assert 100.0 == state.attributes['speed'] + assert 105.32 == state.attributes['direction'] + assert 102.0 == state.attributes['altitude'] + assert 'gps' == state.attributes['provider'] + assert 'running' == state.attributes['activity'] From 452d7cfd610322d4ae7088f975634bba8d35b91b Mon Sep 17 00:00:00 2001 From: Rohan Kapoor Date: Sun, 13 Jan 2019 17:07:45 -0800 Subject: [PATCH 158/235] Return web.Response correctly --- homeassistant/components/locative/__init__.py | 37 ++++++++++++++----- 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/locative/__init__.py b/homeassistant/components/locative/__init__.py index 025b14205b4..b238f6ee659 100644 --- a/homeassistant/components/locative/__init__.py +++ b/homeassistant/components/locative/__init__.py @@ -8,12 +8,13 @@ import logging from typing import Dict import voluptuous as vol +from aiohttp import web import homeassistant.helpers.config_validation as cv from homeassistant.components.device_tracker import \ DOMAIN as DEVICE_TRACKER_DOMAIN from homeassistant.const import HTTP_UNPROCESSABLE_ENTITY, ATTR_LATITUDE, \ - ATTR_LONGITUDE, STATE_NOT_HOME, CONF_WEBHOOK_ID, ATTR_ID + ATTR_LONGITUDE, STATE_NOT_HOME, CONF_WEBHOOK_ID, ATTR_ID, HTTP_OK from homeassistant.helpers import config_entry_flow from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -68,7 +69,10 @@ async def handle_webhook(hass, webhook_id, request): try: data = WEBHOOK_SCHEMA(dict(await request.post())) except vol.MultipleInvalid as error: - return error.error_message, HTTP_UNPROCESSABLE_ENTITY + return web.Response( + body=error.error_message, + status=HTTP_UNPROCESSABLE_ENTITY + ) device = data[ATTR_DEVICE_ID] location_name = data.get(ATTR_ID, data[ATTR_TRIGGER]).lower() @@ -83,7 +87,10 @@ async def handle_webhook(hass, webhook_id, request): gps_location, location_name ) - return 'Setting location to {}'.format(location_name) + return web.Response( + body='Setting location to {}'.format(location_name), + status=HTTP_OK + ) if direction == 'exit': current_state = hass.states.get( @@ -98,24 +105,36 @@ async def handle_webhook(hass, webhook_id, request): gps_location, location_name ) - return 'Setting location to not home' + return web.Response( + body='Setting location to not home', + status=HTTP_OK + ) # Ignore the message if it is telling us to exit a zone that we # aren't currently in. This occurs when a zone is entered # before the previous zone was exited. The enter message will # be sent first, then the exit message will be sent second. - return 'Ignoring exit from {} (already in {})'.format( - location_name, current_state) + return web.Response( + body='Ignoring exit from {} (already in {})'.format( + location_name, current_state + ), + status=HTTP_OK + ) if direction == 'test': # In the app, a test message can be sent. Just return something to # the user to let them know that it works. - return 'Received test message.' + return web.Response( + body='Received test message.', + status=HTTP_OK + ) _LOGGER.error('Received unidentified message from Locative: %s', direction) - return ('Received unidentified message: {}'.format(direction), - HTTP_UNPROCESSABLE_ENTITY) + return web.Response( + body='Received unidentified message: {}'.format(direction), + status=HTTP_UNPROCESSABLE_ENTITY + ) async def async_setup_entry(hass, entry): From 7db28d3d91869cc1b2898342bcfce096930ad449 Mon Sep 17 00:00:00 2001 From: Spencer Oberstadt Date: Mon, 14 Jan 2019 02:44:30 -0500 Subject: [PATCH 159/235] Add Roku hub and remote (#17548) * add roku remote component * remove name config (for now) * update coveragerc and requirements_all * fix linting errors * remove extra requirements entry * fix flake8 errors * remove some references to apple tv * remove redundant REQUIREMENTS * Update requirements_all.txt * Pass hass_config to load_platform * don't expose registry constant * remove unnecessary registry list * use await instead of add_job * use ensure_list * fix code style * some review fixes * code style fixes * stop using async * use add with update * fix whitespace * remove I/O from init loop * move import --- .coveragerc | 4 +- homeassistant/components/discovery.py | 3 +- homeassistant/components/media_player/roku.py | 66 ++-------- homeassistant/components/remote/roku.py | 72 +++++++++++ homeassistant/components/roku.py | 115 ++++++++++++++++++ requirements_all.txt | 2 +- 6 files changed, 203 insertions(+), 59 deletions(-) create mode 100644 homeassistant/components/remote/roku.py create mode 100644 homeassistant/components/roku.py diff --git a/.coveragerc b/.coveragerc index aedf311d6cd..bab0eb8703e 100644 --- a/.coveragerc +++ b/.coveragerc @@ -308,6 +308,9 @@ omit = homeassistant/components/rfxtrx.py homeassistant/components/*/rfxtrx.py + homeassistant/components/roku.py + homeassistant/components/*/roku.py + homeassistant/components/rpi_gpio.py homeassistant/components/*/rpi_gpio.py @@ -642,7 +645,6 @@ omit = homeassistant/components/media_player/pioneer.py homeassistant/components/media_player/pjlink.py homeassistant/components/media_player/plex.py - homeassistant/components/media_player/roku.py homeassistant/components/media_player/russound_rio.py homeassistant/components/media_player/russound_rnet.py homeassistant/components/media_player/snapcast.py diff --git a/homeassistant/components/discovery.py b/homeassistant/components/discovery.py index f87395520bb..d8198ba3033 100644 --- a/homeassistant/components/discovery.py +++ b/homeassistant/components/discovery.py @@ -47,6 +47,7 @@ SERVICE_OCTOPRINT = 'octoprint' SERVICE_FREEBOX = 'freebox' SERVICE_IGD = 'igd' SERVICE_DLNA_DMR = 'dlna_dmr' +SERVICE_ROKU = 'roku' CONFIG_ENTRY_HANDLERS = { SERVICE_DAIKIN: 'daikin', @@ -67,6 +68,7 @@ SERVICE_HANDLERS = { SERVICE_HASSIO: ('hassio', None), SERVICE_AXIS: ('axis', None), SERVICE_APPLE_TV: ('apple_tv', None), + SERVICE_ROKU: ('roku', None), SERVICE_WINK: ('wink', None), SERVICE_XIAOMI_GW: ('xiaomi_aqara', None), SERVICE_SABNZBD: ('sabnzbd', None), @@ -76,7 +78,6 @@ SERVICE_HANDLERS = { SERVICE_FREEBOX: ('freebox', None), 'panasonic_viera': ('media_player', 'panasonic_viera'), 'plex_mediaserver': ('media_player', 'plex'), - 'roku': ('media_player', 'roku'), 'yamaha': ('media_player', 'yamaha'), 'logitech_mediaserver': ('media_player', 'squeezebox'), 'directv': ('media_player', 'directv'), diff --git a/homeassistant/components/media_player/roku.py b/homeassistant/components/media_player/roku.py index fccca235193..20a6f42d729 100644 --- a/homeassistant/components/media_player/roku.py +++ b/homeassistant/components/media_player/roku.py @@ -1,79 +1,38 @@ """ -Support for the roku media player. +Support for the Roku media player. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/media_player.roku/ """ import logging - -import voluptuous as vol +import requests.exceptions from homeassistant.components.media_player import ( - MEDIA_TYPE_MOVIE, PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK, SUPPORT_PLAY, + MEDIA_TYPE_MOVIE, SUPPORT_NEXT_TRACK, SUPPORT_PLAY, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_SELECT_SOURCE, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, MediaPlayerDevice) from homeassistant.const import ( CONF_HOST, STATE_HOME, STATE_IDLE, STATE_PLAYING, STATE_UNKNOWN) -import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['python-roku==3.1.5'] +DEPENDENCIES = ['roku'] -KNOWN_HOSTS = [] DEFAULT_PORT = 8060 -NOTIFICATION_ID = 'roku_notification' -NOTIFICATION_TITLE = 'Roku Media Player Setup' - _LOGGER = logging.getLogger(__name__) SUPPORT_ROKU = SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK |\ SUPPORT_PLAY_MEDIA | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE |\ SUPPORT_SELECT_SOURCE | SUPPORT_PLAY -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_HOST): cv.string, -}) - -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): """Set up the Roku platform.""" - hosts = [] + if not discovery_info: + return - if discovery_info: - host = discovery_info.get('host') - - if host in KNOWN_HOSTS: - return - - _LOGGER.debug("Discovered Roku: %s", host) - hosts.append(discovery_info.get('host')) - - elif CONF_HOST in config: - hosts.append(config.get(CONF_HOST)) - - rokus = [] - for host in hosts: - new_roku = RokuDevice(host) - - try: - if new_roku.name is not None: - rokus.append(RokuDevice(host)) - KNOWN_HOSTS.append(host) - else: - _LOGGER.error("Unable to initialize roku at %s", host) - - except AttributeError: - _LOGGER.error("Unable to initialize roku at %s", host) - hass.components.persistent_notification.create( - 'Error: Unable to initialize roku at {}
' - 'Check its network connection or consider ' - 'using auto discovery.
' - 'You will need to restart hass after fixing.' - ''.format(config.get(CONF_HOST)), - title=NOTIFICATION_TITLE, - notification_id=NOTIFICATION_ID) - - add_entities(rokus) + host = discovery_info[CONF_HOST] + async_add_entities([RokuDevice(host)], True) class RokuDevice(MediaPlayerDevice): @@ -89,12 +48,8 @@ class RokuDevice(MediaPlayerDevice): self.current_app = None self._device_info = {} - self.update() - def update(self): """Retrieve latest state.""" - import requests.exceptions - try: self._device_info = self.roku.device_info self.ip_address = self.roku.host @@ -106,7 +61,6 @@ class RokuDevice(MediaPlayerDevice): self.current_app = None except (requests.exceptions.ConnectionError, requests.exceptions.ReadTimeout): - pass def get_source_list(self): diff --git a/homeassistant/components/remote/roku.py b/homeassistant/components/remote/roku.py new file mode 100644 index 00000000000..86a7105dafe --- /dev/null +++ b/homeassistant/components/remote/roku.py @@ -0,0 +1,72 @@ +""" +Support for the Roku remote. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/remote.roku/ +""" +import requests.exceptions + +from homeassistant.components import remote +from homeassistant.const import (CONF_HOST) + + +DEPENDENCIES = ['roku'] + + +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): + """Set up the Roku remote platform.""" + if not discovery_info: + return + + host = discovery_info[CONF_HOST] + async_add_entities([RokuRemote(host)], True) + + +class RokuRemote(remote.RemoteDevice): + """Device that sends commands to an Roku.""" + + def __init__(self, host): + """Initialize the Roku device.""" + from roku import Roku + + self.roku = Roku(host) + self._device_info = {} + + def update(self): + """Retrieve latest state.""" + try: + self._device_info = self.roku.device_info + except (requests.exceptions.ConnectionError, + requests.exceptions.ReadTimeout): + pass + + @property + def name(self): + """Return the name of the device.""" + if self._device_info.userdevicename: + return self._device_info.userdevicename + return "Roku {}".format(self._device_info.sernum) + + @property + def unique_id(self): + """Return a unique ID.""" + return self._device_info.sernum + + @property + def is_on(self): + """Return true if device is on.""" + return True + + @property + def should_poll(self): + """No polling needed for Roku.""" + return False + + def send_command(self, command, **kwargs): + """Send a command to one device.""" + for single_command in command: + if not hasattr(self.roku, single_command): + continue + + getattr(self.roku, single_command)() diff --git a/homeassistant/components/roku.py b/homeassistant/components/roku.py new file mode 100644 index 00000000000..5ceebb3dee5 --- /dev/null +++ b/homeassistant/components/roku.py @@ -0,0 +1,115 @@ +""" +Support for Roku platform. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/roku/ +""" +import logging + +import voluptuous as vol + +from homeassistant.components.discovery import SERVICE_ROKU +from homeassistant.const import CONF_HOST +from homeassistant.helpers import discovery +import homeassistant.helpers.config_validation as cv + +REQUIREMENTS = ['python-roku==3.1.5'] + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = 'roku' + +SERVICE_SCAN = 'roku_scan' + +ATTR_ROKU = 'roku' + +DATA_ROKU = 'data_roku' + +NOTIFICATION_ID = 'roku_notification' +NOTIFICATION_TITLE = 'Roku Setup' +NOTIFICATION_SCAN_ID = 'roku_scan_notification' +NOTIFICATION_SCAN_TITLE = 'Roku Scan' + + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.All(cv.ensure_list, [vol.Schema({ + vol.Required(CONF_HOST): cv.string + })]) +}, extra=vol.ALLOW_EXTRA) + +# Currently no attributes but it might change later +ROKU_SCAN_SCHEMA = vol.Schema({}) + + +def setup(hass, config): + """Set up the Roku component.""" + hass.data[DATA_ROKU] = {} + + def service_handler(service): + """Handle service calls.""" + if service.service == SERVICE_SCAN: + scan_for_rokus(hass) + + def roku_discovered(service, info): + """Set up an Roku that was auto discovered.""" + _setup_roku(hass, config, { + CONF_HOST: info['host'] + }) + + discovery.listen(hass, SERVICE_ROKU, roku_discovered) + + for conf in config.get(DOMAIN, []): + _setup_roku(hass, config, conf) + + hass.services.register( + DOMAIN, SERVICE_SCAN, service_handler, + schema=ROKU_SCAN_SCHEMA) + + return True + + +def scan_for_rokus(hass): + """Scan for devices and present a notification of the ones found.""" + from roku import Roku, RokuException + rokus = Roku.discover() + + devices = [] + for roku in rokus: + try: + r_info = roku.device_info + except RokuException: # skip non-roku device + continue + devices.append('Name: {0}
Host: {1}
'.format( + r_info.userdevicename if r_info.userdevicename + else "{} {}".format(r_info.modelname, r_info.sernum), + roku.host)) + if not devices: + devices = ['No device(s) found'] + + hass.components.persistent_notification.create( + 'The following devices were found:

' + + '

'.join(devices), + title=NOTIFICATION_SCAN_TITLE, + notification_id=NOTIFICATION_SCAN_ID) + + +def _setup_roku(hass, hass_config, roku_config): + """Set up a Roku.""" + from roku import Roku + host = roku_config[CONF_HOST] + + if host in hass.data[DATA_ROKU]: + return + + roku = Roku(host) + r_info = roku.device_info + + hass.data[DATA_ROKU][host] = { + ATTR_ROKU: r_info.sernum + } + + discovery.load_platform( + hass, 'media_player', DOMAIN, roku_config, hass_config) + + discovery.load_platform( + hass, 'remote', DOMAIN, roku_config, hass_config) diff --git a/requirements_all.txt b/requirements_all.txt index f7f56db88b7..8dd6709898f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1304,7 +1304,7 @@ python-qbittorrent==0.3.1 # homeassistant.components.sensor.ripple python-ripple-api==0.0.3 -# homeassistant.components.media_player.roku +# homeassistant.components.roku python-roku==3.1.5 # homeassistant.components.sensor.sochain From eb610e609373aedf560db7609f04c43fbc06fabe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Mon, 14 Jan 2019 10:05:24 +0200 Subject: [PATCH 160/235] Upgrade pytest to 4.1.1 (#20088) --- 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 a2a52524210..af256efc709 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -13,5 +13,5 @@ pytest-aiohttp==0.3.0 pytest-cov==2.6.1 pytest-sugar==0.9.2 pytest-timeout==1.3.3 -pytest==4.1.0 +pytest==4.1.1 requests_mock==1.5.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bdc9268f9ba..3b5eabfaf6e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -14,7 +14,7 @@ pytest-aiohttp==0.3.0 pytest-cov==2.6.1 pytest-sugar==0.9.2 pytest-timeout==1.3.3 -pytest==4.1.0 +pytest==4.1.1 requests_mock==1.5.2 From d6d28dd3e9daf274e5973972ddbc0236adf4bd39 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 14 Jan 2019 01:49:38 -0800 Subject: [PATCH 161/235] Lowercase code format (#20077) --- homeassistant/components/alarm_control_panel/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/alarm_control_panel/__init__.py b/homeassistant/components/alarm_control_panel/__init__.py index d1e64dc56d7..7f3dc2ac8f5 100644 --- a/homeassistant/components/alarm_control_panel/__init__.py +++ b/homeassistant/components/alarm_control_panel/__init__.py @@ -21,8 +21,8 @@ from homeassistant.helpers.entity_component import EntityComponent DOMAIN = 'alarm_control_panel' SCAN_INTERVAL = timedelta(seconds=30) ATTR_CHANGED_BY = 'changed_by' -FORMAT_TEXT = 'Text' -FORMAT_NUMBER = 'Number' +FORMAT_TEXT = 'text' +FORMAT_NUMBER = 'number' ENTITY_ID_FORMAT = DOMAIN + '.{}' From cb9e0c03d59c57c3e03fe323906e5ddd1500882c Mon Sep 17 00:00:00 2001 From: Thom Troy Date: Mon, 14 Jan 2019 09:51:37 +0000 Subject: [PATCH 162/235] fix logic error in dubln bus (#20075) --- homeassistant/components/sensor/dublin_bus_transport.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/dublin_bus_transport.py b/homeassistant/components/sensor/dublin_bus_transport.py index d47c471e90d..02527f1e333 100644 --- a/homeassistant/components/sensor/dublin_bus_transport.py +++ b/homeassistant/components/sensor/dublin_bus_transport.py @@ -92,7 +92,7 @@ class DublinPublicTransportSensor(Entity): """Return the state attributes.""" if self._times is not None: next_up = "None" - if len(self._times) >= 1: + if len(self._times) > 1: next_up = self._times[1][ATTR_ROUTE] + " in " next_up += self._times[1][ATTR_DUE_IN] From 5652a4a58bd3c99ab64e92c30fe5214d3dd43d2c Mon Sep 17 00:00:00 2001 From: shred86 <32663154+shred86@users.noreply.github.com> Date: Mon, 14 Jan 2019 04:01:59 -0800 Subject: [PATCH 163/235] Bump abode to 0.15.0 (#20064) Fix for motion sensor states --- homeassistant/components/abode.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/abode.py b/homeassistant/components/abode.py index 99bc026a532..8a1a39a726f 100644 --- a/homeassistant/components/abode.py +++ b/homeassistant/components/abode.py @@ -18,7 +18,7 @@ from homeassistant.helpers import config_validation as cv from homeassistant.helpers import discovery from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['abodepy==0.14.0'] +REQUIREMENTS = ['abodepy==0.15.0'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 8dd6709898f..aed3f5cbb37 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -81,7 +81,7 @@ WazeRouteCalculator==0.6 YesssSMS==0.2.3 # homeassistant.components.abode -abodepy==0.14.0 +abodepy==0.15.0 # homeassistant.components.media_player.frontier_silicon afsapi==0.0.4 From fff3cb0b46e00e7e2b784d784476b727f3c17a83 Mon Sep 17 00:00:00 2001 From: Alok Saboo Date: Mon, 14 Jan 2019 07:02:30 -0500 Subject: [PATCH 164/235] Change return text code for alarm control panels (#20055) * Change return text code for alarmdotcom * Change return text code for ialarm * Change return text code for IFTTT * Change return text code for manual alarm panel * Change return text code for manual MQTT alarm * Change return text code for MQTT * Change return text code for Simplisafe --- homeassistant/components/alarm_control_panel/alarmdotcom.py | 2 +- homeassistant/components/alarm_control_panel/ialarm.py | 2 +- homeassistant/components/alarm_control_panel/ifttt.py | 2 +- homeassistant/components/alarm_control_panel/manual.py | 2 +- homeassistant/components/alarm_control_panel/manual_mqtt.py | 2 +- homeassistant/components/alarm_control_panel/mqtt.py | 2 +- homeassistant/components/alarm_control_panel/simplisafe.py | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/alarm_control_panel/alarmdotcom.py b/homeassistant/components/alarm_control_panel/alarmdotcom.py index a650e594d22..03cf9c1ddf8 100644 --- a/homeassistant/components/alarm_control_panel/alarmdotcom.py +++ b/homeassistant/components/alarm_control_panel/alarmdotcom.py @@ -82,7 +82,7 @@ class AlarmDotCom(alarm.AlarmControlPanel): return None if isinstance(self._code, str) and re.search('^\\d+$', self._code): return alarm.FORMAT_NUMBER - return 'Any' + return alarm.FORMAT_TEXT @property def state(self): diff --git a/homeassistant/components/alarm_control_panel/ialarm.py b/homeassistant/components/alarm_control_panel/ialarm.py index abb1862431e..df975ef00ac 100644 --- a/homeassistant/components/alarm_control_panel/ialarm.py +++ b/homeassistant/components/alarm_control_panel/ialarm.py @@ -83,7 +83,7 @@ class IAlarmPanel(alarm.AlarmControlPanel): return None if isinstance(self._code, str) and re.search('^\\d+$', self._code): return alarm.FORMAT_NUMBER - return 'Any' + return alarm.FORMAT_TEXT @property def state(self): diff --git a/homeassistant/components/alarm_control_panel/ifttt.py b/homeassistant/components/alarm_control_panel/ifttt.py index f88f7b3dfab..fe9c96a0083 100644 --- a/homeassistant/components/alarm_control_panel/ifttt.py +++ b/homeassistant/components/alarm_control_panel/ifttt.py @@ -130,7 +130,7 @@ class IFTTTAlarmPanel(alarm.AlarmControlPanel): return None if isinstance(self._code, str) and re.search('^\\d+$', self._code): return alarm.FORMAT_NUMBER - return 'Any' + return alarm.FORMAT_TEXT def alarm_disarm(self, code=None): """Send disarm command.""" diff --git a/homeassistant/components/alarm_control_panel/manual.py b/homeassistant/components/alarm_control_panel/manual.py index 1efed8ebb7b..a36a38f596f 100644 --- a/homeassistant/components/alarm_control_panel/manual.py +++ b/homeassistant/components/alarm_control_panel/manual.py @@ -208,7 +208,7 @@ class ManualAlarm(alarm.AlarmControlPanel, RestoreEntity): return None if isinstance(self._code, str) and re.search('^\\d+$', self._code): return alarm.FORMAT_NUMBER - return 'Any' + return alarm.FORMAT_TEXT def alarm_disarm(self, code=None): """Send disarm command.""" diff --git a/homeassistant/components/alarm_control_panel/manual_mqtt.py b/homeassistant/components/alarm_control_panel/manual_mqtt.py index 48b8c1053c4..693c15fa424 100644 --- a/homeassistant/components/alarm_control_panel/manual_mqtt.py +++ b/homeassistant/components/alarm_control_panel/manual_mqtt.py @@ -242,7 +242,7 @@ class ManualMQTTAlarm(alarm.AlarmControlPanel): return None if isinstance(self._code, str) and re.search('^\\d+$', self._code): return alarm.FORMAT_NUMBER - return 'Any' + return alarm.FORMAT_TEXT def alarm_disarm(self, code=None): """Send disarm command.""" diff --git a/homeassistant/components/alarm_control_panel/mqtt.py b/homeassistant/components/alarm_control_panel/mqtt.py index df583fbbd94..7d092216a1d 100644 --- a/homeassistant/components/alarm_control_panel/mqtt.py +++ b/homeassistant/components/alarm_control_panel/mqtt.py @@ -173,7 +173,7 @@ class MqttAlarm(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, return None if isinstance(code, str) and re.search('^\\d+$', code): return alarm.FORMAT_NUMBER - return 'Any' + return alarm.FORMAT_TEXT async def async_alarm_disarm(self, code=None): """Send disarm command. diff --git a/homeassistant/components/alarm_control_panel/simplisafe.py b/homeassistant/components/alarm_control_panel/simplisafe.py index cf470f15c56..626a819b0b9 100644 --- a/homeassistant/components/alarm_control_panel/simplisafe.py +++ b/homeassistant/components/alarm_control_panel/simplisafe.py @@ -65,7 +65,7 @@ class SimpliSafeAlarm(alarm.AlarmControlPanel): return None if isinstance(self._code, str) and re.search('^\\d+$', self._code): return alarm.FORMAT_NUMBER - return 'Any' + return alarm.FORMAT_TEXT @property def state(self): From ef79566864caf1bcdf3be16784b224d3cc8e31d3 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Mon, 14 Jan 2019 05:12:06 -0700 Subject: [PATCH 165/235] Adjust OpenUV integration for upcoming API limit changes (#19949) * Adjust OpenUV integration for upcoming API limit changes * Added fix for "Invalid API Key" * Bugfix * Add initial nighttime check * Move from polling to a service-based model * Fixed test * Removed unnecessary scan interval * Fixed test * Moving test imports * Member comments * Hound * Removed unused import --- homeassistant/components/openuv/__init__.py | 21 +---- .../components/openuv/config_flow.py | 19 ++--- homeassistant/components/openuv/const.py | 4 - homeassistant/components/openuv/services.yaml | 5 ++ tests/components/openuv/test_config_flow.py | 82 ++++++++++--------- 5 files changed, 60 insertions(+), 71 deletions(-) create mode 100644 homeassistant/components/openuv/services.yaml diff --git a/homeassistant/components/openuv/__init__.py b/homeassistant/components/openuv/__init__.py index bc29910a196..32c3da0d3e5 100644 --- a/homeassistant/components/openuv/__init__.py +++ b/homeassistant/components/openuv/__init__.py @@ -5,7 +5,6 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/openuv/ """ import logging -from datetime import timedelta import voluptuous as vol @@ -13,15 +12,14 @@ from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ( ATTR_ATTRIBUTION, CONF_API_KEY, CONF_BINARY_SENSORS, CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE, CONF_MONITORED_CONDITIONS, - CONF_SCAN_INTERVAL, CONF_SENSORS) + CONF_SENSORS) from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import aiohttp_client, config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.entity import Entity -from homeassistant.helpers.event import async_track_time_interval from .config_flow import configured_instances -from .const import DEFAULT_SCAN_INTERVAL, DOMAIN +from .const import DOMAIN REQUIREMENTS = ['pyopenuv==1.0.4'] _LOGGER = logging.getLogger(__name__) @@ -93,8 +91,6 @@ CONFIG_SCHEMA = vol.Schema({ vol.Optional(CONF_BINARY_SENSORS, default={}): BINARY_SENSOR_SCHEMA, vol.Optional(CONF_SENSORS, default={}): SENSOR_SCHEMA, - vol.Optional(CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL): - cv.time_period, }) }, extra=vol.ALLOW_EXTRA) @@ -120,7 +116,6 @@ async def async_setup(hass, config): CONF_API_KEY: conf[CONF_API_KEY], CONF_BINARY_SENSORS: conf[CONF_BINARY_SENSORS], CONF_SENSORS: conf[CONF_SENSORS], - CONF_SCAN_INTERVAL: conf[CONF_SCAN_INTERVAL], } if CONF_LATITUDE in conf: @@ -167,17 +162,13 @@ async def async_setup_entry(hass, config_entry): hass.config_entries.async_forward_entry_setup( config_entry, component)) - async def refresh(event_time): + async def update_data(service): """Refresh OpenUV data.""" _LOGGER.debug('Refreshing OpenUV data') await openuv.async_update() async_dispatcher_send(hass, TOPIC_UPDATE) - hass.data[DOMAIN][DATA_OPENUV_LISTENER][ - config_entry.entry_id] = async_track_time_interval( - hass, - refresh, - timedelta(seconds=config_entry.data[CONF_SCAN_INTERVAL])) + hass.services.async_register(DOMAIN, 'update_data', update_data) return True @@ -186,10 +177,6 @@ async def async_unload_entry(hass, config_entry): """Unload an OpenUV config entry.""" hass.data[DOMAIN][DATA_OPENUV_CLIENT].pop(config_entry.entry_id) - remove_listener = hass.data[DOMAIN][DATA_OPENUV_LISTENER].pop( - config_entry.entry_id) - remove_listener() - for component in ('binary_sensor', 'sensor'): await hass.config_entries.async_forward_entry_unload( config_entry, component) diff --git a/homeassistant/components/openuv/config_flow.py b/homeassistant/components/openuv/config_flow.py index 11301baf5c5..0f566e5a9ef 100644 --- a/homeassistant/components/openuv/config_flow.py +++ b/homeassistant/components/openuv/config_flow.py @@ -5,11 +5,10 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.core import callback from homeassistant.const import ( - CONF_API_KEY, CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE, - CONF_SCAN_INTERVAL) + CONF_API_KEY, CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE) from homeassistant.helpers import aiohttp_client, config_validation as cv -from .const import DEFAULT_SCAN_INTERVAL, DOMAIN +from .const import DOMAIN @callback @@ -54,7 +53,8 @@ class OpenUvFlowHandler(config_entries.ConfigFlow): async def async_step_user(self, user_input=None): """Handle the start of the config flow.""" - from pyopenuv.util import validate_api_key + from pyopenuv import Client + from pyopenuv.errors import OpenUvError if not user_input: return await self._show_form() @@ -66,14 +66,11 @@ class OpenUvFlowHandler(config_entries.ConfigFlow): return await self._show_form({CONF_LATITUDE: 'identifier_exists'}) websession = aiohttp_client.async_get_clientsession(self.hass) - api_key_validation = await validate_api_key( - user_input[CONF_API_KEY], websession) + client = Client(user_input[CONF_API_KEY], 0, 0, websession) - if not api_key_validation: + try: + await client.uv_index() + except OpenUvError: return await self._show_form({CONF_API_KEY: 'invalid_api_key'}) - scan_interval = user_input.get( - CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) - user_input[CONF_SCAN_INTERVAL] = scan_interval.seconds - return self.async_create_entry(title=identifier, data=user_input) diff --git a/homeassistant/components/openuv/const.py b/homeassistant/components/openuv/const.py index 16623e45642..a8c7fcc0ef5 100644 --- a/homeassistant/components/openuv/const.py +++ b/homeassistant/components/openuv/const.py @@ -1,6 +1,2 @@ """Define constants for the OpenUV component.""" -from datetime import timedelta - DOMAIN = 'openuv' - -DEFAULT_SCAN_INTERVAL = timedelta(minutes=30) diff --git a/homeassistant/components/openuv/services.yaml b/homeassistant/components/openuv/services.yaml new file mode 100644 index 00000000000..f353c7f4774 --- /dev/null +++ b/homeassistant/components/openuv/services.yaml @@ -0,0 +1,5 @@ +# Describes the format for available OpenUV services + +--- +update_data: + description: Request new data from OpenUV. diff --git a/tests/components/openuv/test_config_flow.py b/tests/components/openuv/test_config_flow.py index 60aa990333f..21e3db7df4f 100644 --- a/tests/components/openuv/test_config_flow.py +++ b/tests/components/openuv/test_config_flow.py @@ -1,14 +1,27 @@ """Define tests for the OpenUV config flow.""" -from datetime import timedelta -from unittest.mock import patch +import pytest +from pyopenuv.errors import OpenUvError from homeassistant import data_entry_flow from homeassistant.components.openuv import DOMAIN, config_flow from homeassistant.const import ( - CONF_API_KEY, CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE, - CONF_SCAN_INTERVAL) + CONF_API_KEY, CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE) -from tests.common import MockConfigEntry, mock_coro +from tests.common import MockConfigEntry, MockDependency, mock_coro + + +@pytest.fixture +def uv_index_response(): + """Define a fixture for a successful /uv response.""" + return mock_coro() + + +@pytest.fixture +def mock_pyopenuv(uv_index_response): + """Mock the pyopenuv library.""" + with MockDependency('pyopenuv') as mock_pyopenuv_: + mock_pyopenuv_.Client().uv_index.return_value = uv_index_response + yield mock_pyopenuv_ async def test_duplicate_error(hass): @@ -28,7 +41,9 @@ async def test_duplicate_error(hass): assert result['errors'] == {CONF_LATITUDE: 'identifier_exists'} -async def test_invalid_api_key(hass): +@pytest.mark.parametrize( + 'uv_index_response', [mock_coro(exception=OpenUvError)]) +async def test_invalid_api_key(hass, mock_pyopenuv): """Test that an invalid API key throws an error.""" conf = { CONF_API_KEY: '12345abcde', @@ -40,10 +55,8 @@ async def test_invalid_api_key(hass): flow = config_flow.OpenUvFlowHandler() flow.hass = hass - with patch('pyopenuv.util.validate_api_key', - return_value=mock_coro(False)): - result = await flow.async_step_user(user_input=conf) - assert result['errors'] == {CONF_API_KEY: 'invalid_api_key'} + result = await flow.async_step_user(user_input=conf) + assert result['errors'] == {CONF_API_KEY: 'invalid_api_key'} async def test_show_form(hass): @@ -57,7 +70,7 @@ async def test_show_form(hass): assert result['step_id'] == 'user' -async def test_step_import(hass): +async def test_step_import(hass, mock_pyopenuv): """Test that the import step works.""" conf = { CONF_API_KEY: '12345abcde', @@ -69,44 +82,35 @@ async def test_step_import(hass): flow = config_flow.OpenUvFlowHandler() flow.hass = hass - with patch('pyopenuv.util.validate_api_key', - return_value=mock_coro(True)): - result = await flow.async_step_import(import_config=conf) - - assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result['title'] == '39.128712, -104.9812612' - assert result['data'] == { - CONF_API_KEY: '12345abcde', - CONF_ELEVATION: 59.1234, - CONF_LATITUDE: 39.128712, - CONF_LONGITUDE: -104.9812612, - CONF_SCAN_INTERVAL: 1800, - } + result = await flow.async_step_import(import_config=conf) + assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result['title'] == '39.128712, -104.9812612' + assert result['data'] == { + CONF_API_KEY: '12345abcde', + CONF_ELEVATION: 59.1234, + CONF_LATITUDE: 39.128712, + CONF_LONGITUDE: -104.9812612, + } -async def test_step_user(hass): +async def test_step_user(hass, mock_pyopenuv): """Test that the user step works.""" conf = { CONF_API_KEY: '12345abcde', CONF_ELEVATION: 59.1234, CONF_LATITUDE: 39.128712, CONF_LONGITUDE: -104.9812612, - CONF_SCAN_INTERVAL: timedelta(minutes=5) } flow = config_flow.OpenUvFlowHandler() flow.hass = hass - with patch('pyopenuv.util.validate_api_key', - return_value=mock_coro(True)): - result = await flow.async_step_user(user_input=conf) - - assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result['title'] == '39.128712, -104.9812612' - assert result['data'] == { - CONF_API_KEY: '12345abcde', - CONF_ELEVATION: 59.1234, - CONF_LATITUDE: 39.128712, - CONF_LONGITUDE: -104.9812612, - CONF_SCAN_INTERVAL: 300, - } + result = await flow.async_step_user(user_input=conf) + assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result['title'] == '39.128712, -104.9812612' + assert result['data'] == { + CONF_API_KEY: '12345abcde', + CONF_ELEVATION: 59.1234, + CONF_LATITUDE: 39.128712, + CONF_LONGITUDE: -104.9812612, + } From e505a9b7b495b3fe3724813ab9d6a6cf8b8329f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Morten=20L=C3=BCneborg?= <43782170+mopolus@users.noreply.github.com> Date: Mon, 14 Jan 2019 13:12:57 +0100 Subject: [PATCH 166/235] Fix ihc issues caused by update to defusedxml (#20091) * Update __init__.py * Update __init__.py --- homeassistant/components/ihc/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ihc/__init__.py b/homeassistant/components/ihc/__init__.py index b081f117919..823f9d2657d 100644 --- a/homeassistant/components/ihc/__init__.py +++ b/homeassistant/components/ihc/__init__.py @@ -264,9 +264,9 @@ def get_discovery_info(component_setup, groups, controller_id): 'ihc_id': ihc_id, 'ctrl_id': controller_id, 'product': { - 'name': product.attrib['name'], - 'note': product.attrib['note'], - 'position': product.attrib['position']}, + 'name': product.get('name') or '', + 'note': product.get('note') or '', + 'position': product.get('position') or ''}, 'product_cfg': product_cfg} discovery_data[name] = device return discovery_data From f22557098074c8d0c581ff47b18eb0e9ca5012f8 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Mon, 14 Jan 2019 17:00:48 +0100 Subject: [PATCH 167/235] Move ESPHome Source Files (#20092) * Move ESPHome source files * Update .coveragerc * Update CODEOWNERS --- .coveragerc | 7 ++++++- CODEOWNERS | 1 - .../{binary_sensor/esphome.py => esphome/binary_sensor.py} | 0 .../components/{cover/esphome.py => esphome/cover.py} | 0 .../components/{fan/esphome.py => esphome/fan.py} | 0 .../components/{light/esphome.py => esphome/light.py} | 0 .../components/{sensor/esphome.py => esphome/sensor.py} | 0 .../components/{switch/esphome.py => esphome/switch.py} | 0 8 files changed, 6 insertions(+), 2 deletions(-) rename homeassistant/components/{binary_sensor/esphome.py => esphome/binary_sensor.py} (100%) rename homeassistant/components/{cover/esphome.py => esphome/cover.py} (100%) rename homeassistant/components/{fan/esphome.py => esphome/fan.py} (100%) rename homeassistant/components/{light/esphome.py => esphome/light.py} (100%) rename homeassistant/components/{sensor/esphome.py => esphome/sensor.py} (100%) rename homeassistant/components/{switch/esphome.py => esphome/switch.py} (100%) diff --git a/.coveragerc b/.coveragerc index bab0eb8703e..e0b1e009f2f 100644 --- a/.coveragerc +++ b/.coveragerc @@ -122,7 +122,12 @@ omit = homeassistant/components/*/ecovacs.py homeassistant/components/esphome/__init__.py - homeassistant/components/*/esphome.py + homeassistant/components/esphome/binary_sensor.py + homeassistant/components/esphome/cover.py + homeassistant/components/esphome/fan.py + homeassistant/components/esphome/light.py + homeassistant/components/esphome/sensor.py + homeassistant/components/esphome/switch.py homeassistant/components/eufy.py homeassistant/components/*/eufy.py diff --git a/CODEOWNERS b/CODEOWNERS index 018fbed67f0..ee8c7892070 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -185,7 +185,6 @@ homeassistant/components/edp_redy.py @abmantis homeassistant/components/eight_sleep.py @mezz64 homeassistant/components/*/eight_sleep.py @mezz64 homeassistant/components/esphome/*.py @OttoWinter -homeassistant/components/*/esphome.py @OttoWinter # H homeassistant/components/hive.py @Rendili @KJonline diff --git a/homeassistant/components/binary_sensor/esphome.py b/homeassistant/components/esphome/binary_sensor.py similarity index 100% rename from homeassistant/components/binary_sensor/esphome.py rename to homeassistant/components/esphome/binary_sensor.py diff --git a/homeassistant/components/cover/esphome.py b/homeassistant/components/esphome/cover.py similarity index 100% rename from homeassistant/components/cover/esphome.py rename to homeassistant/components/esphome/cover.py diff --git a/homeassistant/components/fan/esphome.py b/homeassistant/components/esphome/fan.py similarity index 100% rename from homeassistant/components/fan/esphome.py rename to homeassistant/components/esphome/fan.py diff --git a/homeassistant/components/light/esphome.py b/homeassistant/components/esphome/light.py similarity index 100% rename from homeassistant/components/light/esphome.py rename to homeassistant/components/esphome/light.py diff --git a/homeassistant/components/sensor/esphome.py b/homeassistant/components/esphome/sensor.py similarity index 100% rename from homeassistant/components/sensor/esphome.py rename to homeassistant/components/esphome/sensor.py diff --git a/homeassistant/components/switch/esphome.py b/homeassistant/components/esphome/switch.py similarity index 100% rename from homeassistant/components/switch/esphome.py rename to homeassistant/components/esphome/switch.py From d717d9f6bec4e0fc010706c285a9d4f872e28b36 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Mon, 14 Jan 2019 11:42:21 -0700 Subject: [PATCH 168/235] Embed RainMachine platforms into the component (#20066) * Embed RainMachine platforms into the component * Updated CODEOWNERS * Updated .coveragerc --- .coveragerc | 4 +++- CODEOWNERS | 1 - .../rainmachine.py => rainmachine/binary_sensor.py} | 0 .../{sensor/rainmachine.py => rainmachine/sensor.py} | 0 .../{switch/rainmachine.py => rainmachine/switch.py} | 0 5 files changed, 3 insertions(+), 2 deletions(-) rename homeassistant/components/{binary_sensor/rainmachine.py => rainmachine/binary_sensor.py} (100%) rename homeassistant/components/{sensor/rainmachine.py => rainmachine/sensor.py} (100%) rename homeassistant/components/{switch/rainmachine.py => rainmachine/switch.py} (100%) diff --git a/.coveragerc b/.coveragerc index e0b1e009f2f..b55f9617d7a 100644 --- a/.coveragerc +++ b/.coveragerc @@ -303,7 +303,9 @@ omit = homeassistant/components/*/raincloud.py homeassistant/components/rainmachine/__init__.py - homeassistant/components/*/rainmachine.py + homeassistant/components/rainmachine/binary_sensor.py + homeassistant/components/rainmachine/sensor.py + homeassistant/components/rainmachine/switch.py homeassistant/components/raspihats.py homeassistant/components/*/raspihats.py diff --git a/CODEOWNERS b/CODEOWNERS index ee8c7892070..c4ea9d3dabe 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -230,7 +230,6 @@ homeassistant/components/*/qwikswitch.py @kellerza # R homeassistant/components/rainmachine/* @bachya -homeassistant/components/*/rainmachine.py @bachya homeassistant/components/*/random.py @fabaff homeassistant/components/*/rfxtrx.py @danielhiversen diff --git a/homeassistant/components/binary_sensor/rainmachine.py b/homeassistant/components/rainmachine/binary_sensor.py similarity index 100% rename from homeassistant/components/binary_sensor/rainmachine.py rename to homeassistant/components/rainmachine/binary_sensor.py diff --git a/homeassistant/components/sensor/rainmachine.py b/homeassistant/components/rainmachine/sensor.py similarity index 100% rename from homeassistant/components/sensor/rainmachine.py rename to homeassistant/components/rainmachine/sensor.py diff --git a/homeassistant/components/switch/rainmachine.py b/homeassistant/components/rainmachine/switch.py similarity index 100% rename from homeassistant/components/switch/rainmachine.py rename to homeassistant/components/rainmachine/switch.py From b3886820b4f997d31b25ec5f437afdded5fdd42d Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Mon, 14 Jan 2019 11:42:48 -0700 Subject: [PATCH 169/235] Embed SimpliSafe platforms into the component (#20069) * Embed SimpliSafe platforms into the component * Updated CODEOWNERS * Updated .coveragerc --- .coveragerc | 2 +- CODEOWNERS | 1 - .../simplisafe.py => simplisafe/alarm_control_panel.py} | 0 3 files changed, 1 insertion(+), 2 deletions(-) rename homeassistant/components/{alarm_control_panel/simplisafe.py => simplisafe/alarm_control_panel.py} (100%) diff --git a/.coveragerc b/.coveragerc index b55f9617d7a..faf8b89d801 100644 --- a/.coveragerc +++ b/.coveragerc @@ -337,7 +337,7 @@ omit = homeassistant/components/*/sense.py homeassistant/components/simplisafe/__init__.py - homeassistant/components/*/simplisafe.py + homeassistant/components/simplisafe/alarm_control_panel.py homeassistant/components/sisyphus.py homeassistant/components/*/sisyphus.py diff --git a/CODEOWNERS b/CODEOWNERS index c4ea9d3dabe..25c1cb84567 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -235,7 +235,6 @@ homeassistant/components/*/rfxtrx.py @danielhiversen # S homeassistant/components/simplisafe/* @bachya -homeassistant/components/*/simplisafe.py @bachya # T homeassistant/components/tahoma.py @philklei diff --git a/homeassistant/components/alarm_control_panel/simplisafe.py b/homeassistant/components/simplisafe/alarm_control_panel.py similarity index 100% rename from homeassistant/components/alarm_control_panel/simplisafe.py rename to homeassistant/components/simplisafe/alarm_control_panel.py From af2949f85f9c2d00fe6fa19be57be3de15ab26c7 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Mon, 14 Jan 2019 11:44:00 -0700 Subject: [PATCH 170/235] Embed OpenUV platforms into the component (#20072) * Embed OpenUV platforms into the component * Updated CODEOWNERS * Updated .coveragerc --- .coveragerc | 3 ++- CODEOWNERS | 1 - .../{binary_sensor/openuv.py => openuv/binary_sensor.py} | 0 .../components/{sensor/openuv.py => openuv/sensor.py} | 0 4 files changed, 2 insertions(+), 2 deletions(-) rename homeassistant/components/{binary_sensor/openuv.py => openuv/binary_sensor.py} (100%) rename homeassistant/components/{sensor/openuv.py => openuv/sensor.py} (100%) diff --git a/.coveragerc b/.coveragerc index faf8b89d801..80b6305881c 100644 --- a/.coveragerc +++ b/.coveragerc @@ -281,7 +281,8 @@ omit = homeassistant/components/*/opentherm_gw.py homeassistant/components/openuv/__init__.py - homeassistant/components/*/openuv.py + homeassistant/components/openuv/binary_sensor.py + homeassistant/components/openuv/sensor.py homeassistant/components/plum_lightpad.py homeassistant/components/*/plum_lightpad.py diff --git a/CODEOWNERS b/CODEOWNERS index 25c1cb84567..cfb83919b9c 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -218,7 +218,6 @@ homeassistant/components/*/ness_alarm.py @nickw444 # O homeassistant/components/openuv/* @bachya -homeassistant/components/*/openuv.py @bachya # P homeassistant/components/point/* @fredrike diff --git a/homeassistant/components/binary_sensor/openuv.py b/homeassistant/components/openuv/binary_sensor.py similarity index 100% rename from homeassistant/components/binary_sensor/openuv.py rename to homeassistant/components/openuv/binary_sensor.py diff --git a/homeassistant/components/sensor/openuv.py b/homeassistant/components/openuv/sensor.py similarity index 100% rename from homeassistant/components/sensor/openuv.py rename to homeassistant/components/openuv/sensor.py From 0f3b6f17396fe159e6b2cd25511a74545c1ec5bd Mon Sep 17 00:00:00 2001 From: emontnemery Date: Mon, 14 Jan 2019 21:01:42 +0100 Subject: [PATCH 171/235] Reconfigure MQTT lock component if discovery info is changed (#19468) * Reconfigure MQTT lock component if discovery info is changed * Use dict[key] for required config keys and keys with default config schema values. --- homeassistant/components/lock/mqtt.py | 114 ++++++++++++++------------ tests/components/lock/test_mqtt.py | 74 ++++++++++++++++- 2 files changed, 133 insertions(+), 55 deletions(-) diff --git a/homeassistant/components/lock/mqtt.py b/homeassistant/components/lock/mqtt.py index 5574c7e4e59..53bfe6ff7a1 100644 --- a/homeassistant/components/lock/mqtt.py +++ b/homeassistant/components/lock/mqtt.py @@ -14,7 +14,7 @@ from homeassistant.components.mqtt import ( ATTR_DISCOVERY_HASH, CONF_AVAILABILITY_TOPIC, CONF_COMMAND_TOPIC, CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, MqttAvailability, MqttDiscoveryUpdate, - MqttEntityDeviceInfo) + MqttEntityDeviceInfo, subscription) from homeassistant.const import ( CONF_DEVICE, CONF_NAME, CONF_OPTIMISTIC, CONF_VALUE_TEMPLATE) from homeassistant.components import mqtt, lock @@ -51,7 +51,7 @@ PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({ async def async_setup_platform(hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None): """Set up MQTT lock panel through configuration.yaml.""" - await _async_setup_entity(hass, config, async_add_entities) + await _async_setup_entity(config, async_add_entities) async def async_setup_entry(hass, config_entry, async_add_entities): @@ -61,7 +61,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): try: discovery_hash = discovery_payload[ATTR_DISCOVERY_HASH] config = PLATFORM_SCHEMA(discovery_payload) - await _async_setup_entity(hass, config, async_add_entities, + await _async_setup_entity(config, async_add_entities, discovery_hash) except Exception: if discovery_hash: @@ -73,81 +73,83 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_discover) -async def _async_setup_entity(hass, config, async_add_entities, +async def _async_setup_entity(config, async_add_entities, discovery_hash=None): """Set up the MQTT Lock platform.""" - value_template = config.get(CONF_VALUE_TEMPLATE) - if value_template is not None: - value_template.hass = hass - - async_add_entities([MqttLock( - config.get(CONF_NAME), - config.get(CONF_STATE_TOPIC), - config.get(CONF_COMMAND_TOPIC), - config.get(CONF_QOS), - config.get(CONF_RETAIN), - config.get(CONF_PAYLOAD_LOCK), - config.get(CONF_PAYLOAD_UNLOCK), - config.get(CONF_OPTIMISTIC), - value_template, - config.get(CONF_AVAILABILITY_TOPIC), - config.get(CONF_PAYLOAD_AVAILABLE), - config.get(CONF_PAYLOAD_NOT_AVAILABLE), - config.get(CONF_UNIQUE_ID), - config.get(CONF_DEVICE), - discovery_hash, - )]) + async_add_entities([MqttLock(config, discovery_hash)]) class MqttLock(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, LockDevice): """Representation of a lock that can be toggled using MQTT.""" - def __init__(self, name, state_topic, command_topic, qos, retain, - payload_lock, payload_unlock, optimistic, value_template, - availability_topic, payload_available, payload_not_available, - unique_id, device_config, discovery_hash): + def __init__(self, config, discovery_hash): """Initialize the lock.""" + self._config = config + self._unique_id = config.get(CONF_UNIQUE_ID) + self._state = False + self._sub_state = None + self._optimistic = False + + availability_topic = config.get(CONF_AVAILABILITY_TOPIC) + payload_available = config[CONF_PAYLOAD_AVAILABLE] + payload_not_available = config[CONF_PAYLOAD_NOT_AVAILABLE] + qos = config[CONF_QOS] + device_config = config.get(CONF_DEVICE) + MqttAvailability.__init__(self, availability_topic, qos, payload_available, payload_not_available) - MqttDiscoveryUpdate.__init__(self, discovery_hash) + MqttDiscoveryUpdate.__init__(self, discovery_hash, + self.discovery_update) MqttEntityDeviceInfo.__init__(self, device_config) - self._state = False - self._name = name - self._state_topic = state_topic - self._command_topic = command_topic - self._qos = qos - self._retain = retain - self._payload_lock = payload_lock - self._payload_unlock = payload_unlock - self._optimistic = optimistic - self._template = value_template - self._discovery_hash = discovery_hash - self._unique_id = unique_id async def async_added_to_hass(self): """Subscribe to MQTT events.""" await super().async_added_to_hass() + await self._subscribe_topics() + + async def discovery_update(self, discovery_payload): + """Handle updated discovery message.""" + config = PLATFORM_SCHEMA(discovery_payload) + self._config = config + await self.availability_discovery_update(config) + await self._subscribe_topics() + self.async_schedule_update_ha_state() + + async def _subscribe_topics(self): + """(Re)Subscribe to topics.""" + value_template = self._config.get(CONF_VALUE_TEMPLATE) + if value_template is not None: + value_template.hass = self.hass @callback def message_received(topic, payload, qos): """Handle new MQTT messages.""" - if self._template is not None: - payload = self._template.async_render_with_possible_json_value( + if value_template is not None: + payload = value_template.async_render_with_possible_json_value( payload) - if payload == self._payload_lock: + if payload == self._config[CONF_PAYLOAD_LOCK]: self._state = True - elif payload == self._payload_unlock: + elif payload == self._config[CONF_PAYLOAD_UNLOCK]: self._state = False self.async_schedule_update_ha_state() - if self._state_topic is None: + if self._config.get(CONF_STATE_TOPIC) is None: # Force into optimistic mode. self._optimistic = True else: - await mqtt.async_subscribe( - self.hass, self._state_topic, message_received, self._qos) + self._sub_state = await subscription.async_subscribe_topics( + self.hass, self._sub_state, + {'state_topic': {'topic': self._config.get(CONF_STATE_TOPIC), + 'msg_callback': message_received, + 'qos': self._config[CONF_QOS]}}) + + async def async_will_remove_from_hass(self): + """Unsubscribe when removed.""" + self._sub_state = await subscription.async_unsubscribe_topics( + self.hass, self._sub_state) + await MqttAvailability.async_will_remove_from_hass(self) @property def should_poll(self): @@ -157,7 +159,7 @@ class MqttLock(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, @property def name(self): """Return the name of the lock.""" - return self._name + return self._config[CONF_NAME] @property def unique_id(self): @@ -180,8 +182,10 @@ class MqttLock(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, This method is a coroutine. """ mqtt.async_publish( - self.hass, self._command_topic, self._payload_lock, self._qos, - self._retain) + self.hass, self._config[CONF_COMMAND_TOPIC], + self._config[CONF_PAYLOAD_LOCK], + self._config[CONF_QOS], + self._config[CONF_RETAIN]) if self._optimistic: # Optimistically assume that switch has changed state. self._state = True @@ -193,8 +197,10 @@ class MqttLock(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, This method is a coroutine. """ mqtt.async_publish( - self.hass, self._command_topic, self._payload_unlock, self._qos, - self._retain) + self.hass, self._config[CONF_COMMAND_TOPIC], + self._config[CONF_PAYLOAD_UNLOCK], + self._config[CONF_QOS], + self._config[CONF_RETAIN]) if self._optimistic: # Optimistically assume that switch has changed state. self._state = False diff --git a/tests/components/lock/test_mqtt.py b/tests/components/lock/test_mqtt.py index f3b7c45d38a..83ae806d295 100644 --- a/tests/components/lock/test_mqtt.py +++ b/tests/components/lock/test_mqtt.py @@ -1,5 +1,6 @@ """The tests for the MQTT lock platform.""" import json +from unittest.mock import ANY from homeassistant.setup import async_setup_component from homeassistant.const import ( @@ -8,7 +9,8 @@ from homeassistant.components import lock, mqtt from homeassistant.components.mqtt.discovery import async_start from tests.common import ( - async_fire_mqtt_message, async_mock_mqtt_component, MockConfigEntry) + async_fire_mqtt_message, async_mock_mqtt_component, MockConfigEntry, + mock_registry) async def test_controlling_state_via_topic(hass, mqtt_mock): @@ -214,6 +216,40 @@ async def test_discovery_broken(hass, mqtt_mock, caplog): assert state is None +async def test_discovery_update_lock(hass, mqtt_mock, caplog): + """Test update of discovered lock.""" + entry = MockConfigEntry(domain=mqtt.DOMAIN) + await async_start(hass, 'homeassistant', {}, entry) + data1 = ( + '{ "name": "Beer",' + ' "state_topic": "test_topic",' + ' "command_topic": "command_topic",' + ' "availability_topic": "availability_topic1" }' + ) + data2 = ( + '{ "name": "Milk",' + ' "state_topic": "test_topic2",' + ' "command_topic": "command_topic",' + ' "availability_topic": "availability_topic2" }' + ) + async_fire_mqtt_message(hass, 'homeassistant/lock/bla/config', + data1) + await hass.async_block_till_done() + state = hass.states.get('lock.beer') + assert state is not None + assert state.name == 'Beer' + async_fire_mqtt_message(hass, 'homeassistant/lock/bla/config', + data2) + await hass.async_block_till_done() + await hass.async_block_till_done() + state = hass.states.get('lock.beer') + assert state is not None + assert state.name == 'Milk' + + state = hass.states.get('lock.milk') + assert state is None + + async def test_entity_device_info_with_identifier(hass, mqtt_mock): """Test MQTT lock device registry integration.""" entry = MockConfigEntry(domain=mqtt.DOMAIN) @@ -251,3 +287,39 @@ async def test_entity_device_info_with_identifier(hass, mqtt_mock): assert device.name == 'Beer' assert device.model == 'Glass' assert device.sw_version == '0.1-beta' + + +async def test_entity_id_update(hass, mqtt_mock): + """Test MQTT subscriptions are managed when entity_id is updated.""" + registry = mock_registry(hass, {}) + mock_mqtt = await async_mock_mqtt_component(hass) + assert await async_setup_component(hass, lock.DOMAIN, { + lock.DOMAIN: [{ + 'platform': 'mqtt', + 'name': 'beer', + 'state_topic': 'test-topic', + 'command_topic': 'test-topic', + 'availability_topic': 'avty-topic', + 'unique_id': 'TOTALLY_UNIQUE' + }] + }) + + state = hass.states.get('lock.beer') + assert state is not None + assert mock_mqtt.async_subscribe.call_count == 2 + mock_mqtt.async_subscribe.assert_any_call('test-topic', ANY, 0, 'utf-8') + mock_mqtt.async_subscribe.assert_any_call('avty-topic', ANY, 0, 'utf-8') + mock_mqtt.async_subscribe.reset_mock() + + registry.async_update_entity('lock.beer', new_entity_id='lock.milk') + await hass.async_block_till_done() + await hass.async_block_till_done() + + state = hass.states.get('lock.beer') + assert state is None + + state = hass.states.get('lock.milk') + assert state is not None + assert mock_mqtt.async_subscribe.call_count == 2 + mock_mqtt.async_subscribe.assert_any_call('test-topic', ANY, 0, 'utf-8') + mock_mqtt.async_subscribe.assert_any_call('avty-topic', ANY, 0, 'utf-8') From e73569c203f21898e660eebb7d1cd04794554160 Mon Sep 17 00:00:00 2001 From: Andrew Sayre <6730289+andrewsayre@users.noreply.github.com> Date: Mon, 14 Jan 2019 17:08:44 -0600 Subject: [PATCH 172/235] Added partial detection to async_add_job (#20119) --- homeassistant/core.py | 11 +++++++--- tests/test_core.py | 50 ++++++++++++++++++++++++++++++++++--------- 2 files changed, 48 insertions(+), 13 deletions(-) diff --git a/homeassistant/core.py b/homeassistant/core.py index 37d1134ef29..2834730408e 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -8,6 +8,7 @@ import asyncio from concurrent.futures import ThreadPoolExecutor import datetime import enum +import functools import logging import os import pathlib @@ -258,11 +259,15 @@ class HomeAssistant: """ task = None - if asyncio.iscoroutine(target): + check_target = target + if isinstance(target, functools.partial): + check_target = target.func + + if asyncio.iscoroutine(check_target): task = self.loop.create_task(target) # type: ignore - elif is_callback(target): + elif is_callback(check_target): self.loop.call_soon(target, *args) - elif asyncio.iscoroutinefunction(target): + elif asyncio.iscoroutinefunction(check_target): task = self.loop.create_task(target(*args)) else: task = self.loop.run_in_executor( # type: ignore diff --git a/tests/test_core.py b/tests/test_core.py index 5ee9f5cdb05..f1900979bec 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -1,6 +1,7 @@ """Test to verify that Home Assistant core works.""" # pylint: disable=protected-access import asyncio +import functools import logging import os import unittest @@ -45,11 +46,24 @@ def test_async_add_job_schedule_callback(): assert len(hass.add_job.mock_calls) == 0 -@patch('asyncio.iscoroutinefunction', return_value=True) -def test_async_add_job_schedule_coroutinefunction(mock_iscoro): - """Test that we schedule coroutines and add jobs to the job pool.""" +def test_async_add_job_schedule_partial_callback(): + """Test that we schedule partial coros and add jobs to the job pool.""" hass = MagicMock() job = MagicMock() + partial = functools.partial(ha.callback(job)) + + ha.HomeAssistant.async_add_job(hass, partial) + assert len(hass.loop.call_soon.mock_calls) == 1 + assert len(hass.loop.create_task.mock_calls) == 0 + assert len(hass.add_job.mock_calls) == 0 + + +def test_async_add_job_schedule_coroutinefunction(): + """Test that we schedule coroutines and add jobs to the job pool.""" + hass = MagicMock() + + async def job(): + pass ha.HomeAssistant.async_add_job(hass, job) assert len(hass.loop.call_soon.mock_calls) == 0 @@ -57,11 +71,26 @@ def test_async_add_job_schedule_coroutinefunction(mock_iscoro): assert len(hass.add_job.mock_calls) == 0 -@patch('asyncio.iscoroutinefunction', return_value=False) -def test_async_add_job_add_threaded_job_to_pool(mock_iscoro): +def test_async_add_job_schedule_partial_coroutinefunction(): + """Test that we schedule partial coros and add jobs to the job pool.""" + hass = MagicMock() + + async def job(): + pass + partial = functools.partial(job) + + ha.HomeAssistant.async_add_job(hass, partial) + assert len(hass.loop.call_soon.mock_calls) == 0 + assert len(hass.loop.create_task.mock_calls) == 1 + assert len(hass.add_job.mock_calls) == 0 + + +def test_async_add_job_add_threaded_job_to_pool(): """Test that we schedule coroutines and add jobs to the job pool.""" hass = MagicMock() - job = MagicMock() + + def job(): + pass ha.HomeAssistant.async_add_job(hass, job) assert len(hass.loop.call_soon.mock_calls) == 0 @@ -69,13 +98,14 @@ def test_async_add_job_add_threaded_job_to_pool(mock_iscoro): assert len(hass.loop.run_in_executor.mock_calls) == 1 -@patch('asyncio.iscoroutine', return_value=True) -def test_async_create_task_schedule_coroutine(mock_iscoro): +def test_async_create_task_schedule_coroutine(): """Test that we schedule coroutines and add jobs to the job pool.""" hass = MagicMock() - job = MagicMock() - ha.HomeAssistant.async_create_task(hass, job) + async def job(): + pass + + ha.HomeAssistant.async_create_task(hass, job()) assert len(hass.loop.call_soon.mock_calls) == 0 assert len(hass.loop.create_task.mock_calls) == 1 assert len(hass.add_job.mock_calls) == 0 From c8d885fb78d747478dbd0e5253ff6d9bcf804f9c Mon Sep 17 00:00:00 2001 From: Fredrik Erlandsson Date: Tue, 15 Jan 2019 06:36:17 +0100 Subject: [PATCH 173/235] Fix tellduslive discovery and auth issues (#20023) * fix for #19954, discovered tellsticks shows up to be configured * fix for #19954, authentication issues * updated tests * move I/O to executer thread pool * Apply suggestions from code review Co-Authored-By: fredrike --- .../tellduslive/.translations/en.json | 5 +- .../components/tellduslive/config_flow.py | 71 +++++++++---------- .../components/tellduslive/strings.json | 42 +++++------ .../tellduslive/test_config_flow.py | 16 ++++- 4 files changed, 75 insertions(+), 59 deletions(-) diff --git a/homeassistant/components/tellduslive/.translations/en.json b/homeassistant/components/tellduslive/.translations/en.json index 3a72b41e1f7..4ed9ef597f4 100644 --- a/homeassistant/components/tellduslive/.translations/en.json +++ b/homeassistant/components/tellduslive/.translations/en.json @@ -1,11 +1,14 @@ { "config": { "abort": { - "all_configured": "TelldusLive is already configured", + "already_setup": "TelldusLive is already configured", "authorize_url_fail": "Unknown error generating an authorize url.", "authorize_url_timeout": "Timeout generating authorize url.", "unknown": "Unknown error occurred" }, + "error": { + "auth_error": "Authentication error, please try again" + }, "step": { "auth": { "description": "To link your TelldusLive account:\n 1. Click the link below\n 2. Login to Telldus Live\n 3. Authorize **{app_name}** (click **Yes**).\n 4. Come back here and click **SUBMIT**.\n\n [Link TelldusLive account]({auth_url})", diff --git a/homeassistant/components/tellduslive/config_flow.py b/homeassistant/components/tellduslive/config_flow.py index 64260b6047c..3373e9cc2f7 100644 --- a/homeassistant/components/tellduslive/config_flow.py +++ b/homeassistant/components/tellduslive/config_flow.py @@ -35,6 +35,13 @@ class FlowHandler(config_entries.ConfigFlow): self._scan_interval = SCAN_INTERVAL def _get_auth_url(self): + from tellduslive import Session + self._session = Session( + public_key=PUBLIC_KEY, + private_key=NOT_SO_PRIVATE_KEY, + host=self._host, + application=APPLICATION_NAME, + ) return self._session.authorize_url async def async_step_user(self, user_input=None): @@ -56,38 +63,36 @@ class FlowHandler(config_entries.ConfigFlow): async def async_step_auth(self, user_input=None): """Handle the submitted configuration.""" - if not self._session: - from tellduslive import Session - self._session = Session( - public_key=PUBLIC_KEY, - private_key=NOT_SO_PRIVATE_KEY, - host=self._host, - application=APPLICATION_NAME, - ) - - if user_input is not None and self._session.authorize(): - host = self._host or CLOUD_NAME - if self._host: - session = { - KEY_HOST: host, - KEY_TOKEN: self._session.access_token - } + errors = {} + if user_input is not None: + if await self.hass.async_add_executor_job( + self._session.authorize): + host = self._host or CLOUD_NAME + if self._host: + session = { + KEY_HOST: host, + KEY_TOKEN: self._session.access_token + } + else: + session = { + KEY_TOKEN: self._session.access_token, + KEY_TOKEN_SECRET: self._session.access_token_secret + } + return self.async_create_entry( + title=host, data={ + KEY_HOST: host, + KEY_SCAN_INTERVAL: self._scan_interval.seconds, + KEY_SESSION: session, + }) else: - session = { - KEY_TOKEN: self._session.access_token, - KEY_TOKEN_SECRET: self._session.access_token_secret - } - return self.async_create_entry( - title=host, data={ - KEY_HOST: host, - KEY_SCAN_INTERVAL: self._scan_interval.seconds, - KEY_SESSION: session, - }) + errors['base'] = 'auth_error' try: with async_timeout.timeout(10): auth_url = await self.hass.async_add_executor_job( self._get_auth_url) + if not auth_url: + return self.async_abort(reason='authorize_url_fail') except asyncio.TimeoutError: return self.async_abort(reason='authorize_url_timeout') except Exception: # pylint: disable=broad-except @@ -97,6 +102,7 @@ class FlowHandler(config_entries.ConfigFlow): _LOGGER.debug('Got authorization URL %s', auth_url) return self.async_show_form( step_id='auth', + errors=errors, description_placeholders={ 'app_name': APPLICATION_NAME, 'auth_url': auth_url, @@ -107,17 +113,10 @@ class FlowHandler(config_entries.ConfigFlow): """Run when a Tellstick is discovered.""" from tellduslive import supports_local_api _LOGGER.info('Discovered tellstick device: %s', user_input) - # Ignore any known devices - for entry in self._async_current_entries(): - if entry.data[KEY_HOST] == user_input[0]: - return self.async_abort(reason='already_configured') + if supports_local_api(user_input[1]): + _LOGGER.info('%s support local API', user_input[1]) + self._hosts.append(user_input[0]) - if not supports_local_api(user_input[1]): - _LOGGER.debug('Tellstick does not support local API') - # Configure the cloud service - return await self.async_step_auth() - - self._hosts.append(user_input[0]) return await self.async_step_user() async def async_step_import(self, user_input): diff --git a/homeassistant/components/tellduslive/strings.json b/homeassistant/components/tellduslive/strings.json index 7be98213222..bb62889085b 100644 --- a/homeassistant/components/tellduslive/strings.json +++ b/homeassistant/components/tellduslive/strings.json @@ -1,24 +1,26 @@ { "config": { - "title": "Telldus Live", - "step": { - "user": { - "title": "Pick endpoint.", - "description": "", - "data": { - "host": "Host" - } + "abort": { + "already_setup": "TelldusLive is already configured", + "authorize_url_fail": "Unknown error generating an authorize url.", + "authorize_url_timeout": "Timeout generating authorize url.", + "unknown": "Unknown error occurred" }, - "auth": { - "title": "Authenticate against TelldusLive", - "description": "To link your TelldusLive account:\n 1. Click the link below\n 2. Login to Telldus Live\n 3. Authorize **{app_name}** (click **Yes**).\n 4. Come back here and click **SUBMIT**.\n\n [Link TelldusLive account]({auth_url})" - } - }, - "abort": { - "authorize_url_timeout": "Timeout generating authorize url.", - "authorize_url_fail": "Unknown error generating an authorize url.", - "all_configured": "TelldusLive is already configured", - "unknown": "Unknown error occurred" - } + "error": { + "auth_error": "Authentication error, please try again" + }, + "step": { + "auth": { + "description": "To link your TelldusLive account:\n 1. Click the link below\n 2. Login to Telldus Live\n 3. Authorize **{app_name}** (click **Yes**).\n 4. Come back here and click **SUBMIT**.\n\n [Link TelldusLive account]({auth_url})", + "title": "Authenticate against TelldusLive" + }, + "user": { + "data": { + "host": "Host" + }, + "title": "Pick endpoint." + } + }, + "title": "Telldus Live" } -} +} \ No newline at end of file diff --git a/tests/components/tellduslive/test_config_flow.py b/tests/components/tellduslive/test_config_flow.py index ba569a9f149..5be9a03742e 100644 --- a/tests/components/tellduslive/test_config_flow.py +++ b/tests/components/tellduslive/test_config_flow.py @@ -67,6 +67,7 @@ async def test_full_flow_implementation(hass, mock_tellduslive): result = await flow.async_step_discovery(['localhost', 'tellstick']) assert result['type'] == data_entry_flow.RESULT_TYPE_FORM assert result['step_id'] == 'user' + assert len(flow._hosts) == 2 result = await flow.async_step_user() assert result['type'] == data_entry_flow.RESULT_TYPE_FORM @@ -156,12 +157,14 @@ async def test_step_disco_no_local_api(hass, mock_tellduslive): result = await flow.async_step_discovery(['localhost', 'tellstick']) assert result['type'] == data_entry_flow.RESULT_TYPE_FORM assert result['step_id'] == 'auth' + assert len(flow._hosts) == 1 async def test_step_auth(hass, mock_tellduslive): """Test that create cloud entity from auth.""" flow = init_config_flow(hass) + await flow.async_step_auth() result = await flow.async_step_auth(['localhost', 'tellstick']) assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result['title'] == 'Cloud API' @@ -178,10 +181,11 @@ async def test_wrong_auth_flow_implementation(hass, mock_tellduslive): """Test wrong auth.""" flow = init_config_flow(hass) - await flow.async_step_user() + await flow.async_step_auth() result = await flow.async_step_auth('') assert result['type'] == data_entry_flow.RESULT_TYPE_FORM assert result['step_id'] == 'auth' + assert result['errors']['base'] == 'auth_error' async def test_not_pick_host_if_only_one(hass, mock_tellduslive): @@ -201,6 +205,14 @@ async def test_abort_if_timeout_generating_auth_url(hass, mock_tellduslive): assert result['type'] == data_entry_flow.RESULT_TYPE_ABORT assert result['reason'] == 'authorize_url_timeout' +async def test_abort_no_auth_url(hass, mock_tellduslive): + """Test abort if generating authorize url returns none.""" + flow = init_config_flow(hass) + flow._get_auth_url = Mock(return_value=False) + + result = await flow.async_step_user() + assert result['type'] == data_entry_flow.RESULT_TYPE_ABORT + assert result['reason'] == 'authorize_url_fail' async def test_abort_if_exception_generating_auth_url(hass, mock_tellduslive): """Test we abort if generating authorize url blows up.""" @@ -220,4 +232,4 @@ async def test_discovery_already_configured(hass, mock_tellduslive): result = await flow.async_step_discovery(['some-host', '']) assert result['type'] == data_entry_flow.RESULT_TYPE_ABORT - assert result['reason'] == 'already_configured' + assert result['reason'] == 'already_setup' From 80bc42af4feec06b345af43ecaeac33c629a352c Mon Sep 17 00:00:00 2001 From: Rohan Kapoor Date: Tue, 15 Jan 2019 03:50:09 -0800 Subject: [PATCH 174/235] Use voluptuous to perform validation for the geofency webhook (#20067) * Use voluptuous to perform validation for the geofency webhook * Add missing attribute to schema * Lint --- homeassistant/components/geofency/__init__.py | 68 ++++++++----------- 1 file changed, 30 insertions(+), 38 deletions(-) diff --git a/homeassistant/components/geofency/__init__.py b/homeassistant/components/geofency/__init__.py index 1e2f368d5b3..093ebaa2fd3 100644 --- a/homeassistant/components/geofency/__init__.py +++ b/homeassistant/components/geofency/__init__.py @@ -11,7 +11,7 @@ from aiohttp import web import homeassistant.helpers.config_validation as cv from homeassistant.const import HTTP_UNPROCESSABLE_ENTITY, STATE_NOT_HOME, \ - ATTR_LATITUDE, ATTR_LONGITUDE, CONF_WEBHOOK_ID, HTTP_OK + ATTR_LATITUDE, ATTR_LONGITUDE, CONF_WEBHOOK_ID, HTTP_OK, ATTR_NAME from homeassistant.helpers import config_entry_flow from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -33,8 +33,12 @@ CONFIG_SCHEMA = vol.Schema({ }), }, extra=vol.ALLOW_EXTRA) +ATTR_ADDRESS = 'address' +ATTR_BEACON_ID = 'beaconUUID' ATTR_CURRENT_LATITUDE = 'currentLatitude' ATTR_CURRENT_LONGITUDE = 'currentLongitude' +ATTR_DEVICE = 'device' +ATTR_ENTRY = 'entry' BEACON_DEV_PREFIX = 'beacon' @@ -44,6 +48,24 @@ LOCATION_EXIT = '0' TRACKER_UPDATE = '{}_tracker_update'.format(DOMAIN) +def _address(value: str) -> str: + r"""Coerce address by replacing '\n' with ' '.""" + return value.replace('\n', ' ') + + +WEBHOOK_SCHEMA = vol.Schema({ + vol.Required(ATTR_ADDRESS): vol.All(cv.string, _address), + vol.Required(ATTR_DEVICE): vol.All(cv.string, slugify), + vol.Required(ATTR_ENTRY): vol.Any(LOCATION_ENTRY, LOCATION_EXIT), + vol.Required(ATTR_LATITUDE): cv.latitude, + vol.Required(ATTR_LONGITUDE): cv.longitude, + vol.Required(ATTR_NAME): vol.All(cv.string, slugify), + vol.Optional(ATTR_CURRENT_LATITUDE): cv.latitude, + vol.Optional(ATTR_CURRENT_LONGITUDE): cv.longitude, + vol.Optional(ATTR_BEACON_ID): cv.string +}, extra=vol.ALLOW_EXTRA) + + async def async_setup(hass, hass_config): """Set up the Geofency component.""" config = hass_config[DOMAIN] @@ -57,12 +79,12 @@ async def async_setup(hass, hass_config): async def handle_webhook(hass, webhook_id, request): - """Handle incoming webhook with Mailgun inbound messages.""" - data = _validate_data(await request.post()) - - if not data: + """Handle incoming webhook from Geofency.""" + try: + data = WEBHOOK_SCHEMA(dict(await request.post())) + except vol.MultipleInvalid as error: return web.Response( - body="Invalid data", + body=error.error_message, status=HTTP_UNPROCESSABLE_ENTITY ) @@ -79,44 +101,14 @@ async def handle_webhook(hass, webhook_id, request): return _set_location(hass, data, location_name) -def _validate_data(data): - """Validate POST payload.""" - data = data.copy() - - required_attributes = ['address', 'device', 'entry', - 'latitude', 'longitude', 'name'] - - valid = True - for attribute in required_attributes: - if attribute not in data: - valid = False - _LOGGER.error("'%s' not specified in message", attribute) - - if not valid: - return {} - - data['address'] = data['address'].replace('\n', ' ') - data['device'] = slugify(data['device']) - data['name'] = slugify(data['name']) - - gps_attributes = [ATTR_LATITUDE, ATTR_LONGITUDE, - ATTR_CURRENT_LATITUDE, ATTR_CURRENT_LONGITUDE] - - for attribute in gps_attributes: - if attribute in data: - data[attribute] = float(data[attribute]) - - return data - - def _is_mobile_beacon(data, mobile_beacons): """Check if we have a mobile beacon.""" - return 'beaconUUID' in data and data['name'] in mobile_beacons + return ATTR_BEACON_ID in data and data['name'] in mobile_beacons def _device_name(data): """Return name of device tracker.""" - if 'beaconUUID' in data: + if ATTR_BEACON_ID in data: return "{}_{}".format(BEACON_DEV_PREFIX, data['name']) return data['device'] From 5fd1053a388d32ea80c362ae937150ce2cdcc938 Mon Sep 17 00:00:00 2001 From: Eliseo Martelli Date: Tue, 15 Jan 2019 13:39:43 +0100 Subject: [PATCH 175/235] fixed gtt to report isotime (#20128) --- homeassistant/components/sensor/gtt.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/gtt.py b/homeassistant/components/sensor/gtt.py index 33175d0c071..f0e141f3549 100644 --- a/homeassistant/components/sensor/gtt.py +++ b/homeassistant/components/sensor/gtt.py @@ -13,7 +13,6 @@ from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import DEVICE_CLASS_TIMESTAMP from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv -import homeassistant.util.dt as dt_util REQUIREMENTS = ['pygtt==1.1.2'] @@ -80,7 +79,8 @@ class GttSensor(Entity): def update(self): """Update device state.""" self.data.get_data() - next_time = dt_util.parse_time(self.data.state_bus['time'][0]['run']) + next_time = datetime.strptime( + self.data.state_bus['time'][0]['run'], "%H:%M") self._state = next_time.isoformat() From 5b53bd6aa02a45ddcd4bf4358e74ddbc0285d8d3 Mon Sep 17 00:00:00 2001 From: emontnemery Date: Tue, 15 Jan 2019 17:31:06 +0100 Subject: [PATCH 176/235] Move MQTT platforms under the component (#20050) * Move MQTT platforms under the component --- .../mqtt.py => mqtt/alarm_control_panel.py} | 0 .../{binary_sensor/mqtt.py => mqtt/binary_sensor.py} | 0 homeassistant/components/{camera/mqtt.py => mqtt/camera.py} | 0 .../components/{climate/mqtt.py => mqtt/climate.py} | 0 homeassistant/components/{cover/mqtt.py => mqtt/cover.py} | 0 homeassistant/components/{fan/mqtt.py => mqtt/fan.py} | 0 .../components/{light/mqtt => mqtt/light}/__init__.py | 0 .../components/{light/mqtt => mqtt/light}/schema_basic.py | 0 .../components/{light/mqtt => mqtt/light}/schema_json.py | 0 .../components/{light/mqtt => mqtt/light}/schema_template.py | 0 homeassistant/components/{lock/mqtt.py => mqtt/lock.py} | 0 homeassistant/components/{sensor/mqtt.py => mqtt/sensor.py} | 0 homeassistant/components/{switch/mqtt.py => mqtt/switch.py} | 0 homeassistant/components/{vacuum/mqtt.py => mqtt/vacuum.py} | 0 .../test_mqtt.py => mqtt/test_alarm_control_panel.py} | 0 .../test_mqtt.py => mqtt/test_binary_sensor.py} | 0 .../components/{camera/test_mqtt.py => mqtt/test_camera.py} | 0 .../{climate/test_mqtt.py => mqtt/test_climate.py} | 0 tests/components/{cover/test_mqtt.py => mqtt/test_cover.py} | 2 +- tests/components/{fan/test_mqtt.py => mqtt/test_fan.py} | 0 tests/components/{light/test_mqtt.py => mqtt/test_light.py} | 0 .../{light/test_mqtt_json.py => mqtt/test_light_json.py} | 0 .../test_mqtt_template.py => mqtt/test_light_template.py} | 0 tests/components/{lock/test_mqtt.py => mqtt/test_lock.py} | 0 .../components/{sensor/test_mqtt.py => mqtt/test_sensor.py} | 4 ++-- .../components/{switch/test_mqtt.py => mqtt/test_switch.py} | 0 .../components/{vacuum/test_mqtt.py => mqtt/test_vacuum.py} | 5 +++-- 27 files changed, 6 insertions(+), 5 deletions(-) rename homeassistant/components/{alarm_control_panel/mqtt.py => mqtt/alarm_control_panel.py} (100%) rename homeassistant/components/{binary_sensor/mqtt.py => mqtt/binary_sensor.py} (100%) rename homeassistant/components/{camera/mqtt.py => mqtt/camera.py} (100%) rename homeassistant/components/{climate/mqtt.py => mqtt/climate.py} (100%) rename homeassistant/components/{cover/mqtt.py => mqtt/cover.py} (100%) rename homeassistant/components/{fan/mqtt.py => mqtt/fan.py} (100%) rename homeassistant/components/{light/mqtt => mqtt/light}/__init__.py (100%) rename homeassistant/components/{light/mqtt => mqtt/light}/schema_basic.py (100%) rename homeassistant/components/{light/mqtt => mqtt/light}/schema_json.py (100%) rename homeassistant/components/{light/mqtt => mqtt/light}/schema_template.py (100%) rename homeassistant/components/{lock/mqtt.py => mqtt/lock.py} (100%) rename homeassistant/components/{sensor/mqtt.py => mqtt/sensor.py} (100%) rename homeassistant/components/{switch/mqtt.py => mqtt/switch.py} (100%) rename homeassistant/components/{vacuum/mqtt.py => mqtt/vacuum.py} (100%) rename tests/components/{alarm_control_panel/test_mqtt.py => mqtt/test_alarm_control_panel.py} (100%) rename tests/components/{binary_sensor/test_mqtt.py => mqtt/test_binary_sensor.py} (100%) rename tests/components/{camera/test_mqtt.py => mqtt/test_camera.py} (100%) rename tests/components/{climate/test_mqtt.py => mqtt/test_climate.py} (100%) rename tests/components/{cover/test_mqtt.py => mqtt/test_cover.py} (99%) rename tests/components/{fan/test_mqtt.py => mqtt/test_fan.py} (100%) rename tests/components/{light/test_mqtt.py => mqtt/test_light.py} (100%) rename tests/components/{light/test_mqtt_json.py => mqtt/test_light_json.py} (100%) rename tests/components/{light/test_mqtt_template.py => mqtt/test_light_template.py} (100%) rename tests/components/{lock/test_mqtt.py => mqtt/test_lock.py} (100%) rename tests/components/{sensor/test_mqtt.py => mqtt/test_sensor.py} (99%) rename tests/components/{switch/test_mqtt.py => mqtt/test_switch.py} (100%) rename tests/components/{vacuum/test_mqtt.py => mqtt/test_vacuum.py} (99%) diff --git a/homeassistant/components/alarm_control_panel/mqtt.py b/homeassistant/components/mqtt/alarm_control_panel.py similarity index 100% rename from homeassistant/components/alarm_control_panel/mqtt.py rename to homeassistant/components/mqtt/alarm_control_panel.py diff --git a/homeassistant/components/binary_sensor/mqtt.py b/homeassistant/components/mqtt/binary_sensor.py similarity index 100% rename from homeassistant/components/binary_sensor/mqtt.py rename to homeassistant/components/mqtt/binary_sensor.py diff --git a/homeassistant/components/camera/mqtt.py b/homeassistant/components/mqtt/camera.py similarity index 100% rename from homeassistant/components/camera/mqtt.py rename to homeassistant/components/mqtt/camera.py diff --git a/homeassistant/components/climate/mqtt.py b/homeassistant/components/mqtt/climate.py similarity index 100% rename from homeassistant/components/climate/mqtt.py rename to homeassistant/components/mqtt/climate.py diff --git a/homeassistant/components/cover/mqtt.py b/homeassistant/components/mqtt/cover.py similarity index 100% rename from homeassistant/components/cover/mqtt.py rename to homeassistant/components/mqtt/cover.py diff --git a/homeassistant/components/fan/mqtt.py b/homeassistant/components/mqtt/fan.py similarity index 100% rename from homeassistant/components/fan/mqtt.py rename to homeassistant/components/mqtt/fan.py diff --git a/homeassistant/components/light/mqtt/__init__.py b/homeassistant/components/mqtt/light/__init__.py similarity index 100% rename from homeassistant/components/light/mqtt/__init__.py rename to homeassistant/components/mqtt/light/__init__.py diff --git a/homeassistant/components/light/mqtt/schema_basic.py b/homeassistant/components/mqtt/light/schema_basic.py similarity index 100% rename from homeassistant/components/light/mqtt/schema_basic.py rename to homeassistant/components/mqtt/light/schema_basic.py diff --git a/homeassistant/components/light/mqtt/schema_json.py b/homeassistant/components/mqtt/light/schema_json.py similarity index 100% rename from homeassistant/components/light/mqtt/schema_json.py rename to homeassistant/components/mqtt/light/schema_json.py diff --git a/homeassistant/components/light/mqtt/schema_template.py b/homeassistant/components/mqtt/light/schema_template.py similarity index 100% rename from homeassistant/components/light/mqtt/schema_template.py rename to homeassistant/components/mqtt/light/schema_template.py diff --git a/homeassistant/components/lock/mqtt.py b/homeassistant/components/mqtt/lock.py similarity index 100% rename from homeassistant/components/lock/mqtt.py rename to homeassistant/components/mqtt/lock.py diff --git a/homeassistant/components/sensor/mqtt.py b/homeassistant/components/mqtt/sensor.py similarity index 100% rename from homeassistant/components/sensor/mqtt.py rename to homeassistant/components/mqtt/sensor.py diff --git a/homeassistant/components/switch/mqtt.py b/homeassistant/components/mqtt/switch.py similarity index 100% rename from homeassistant/components/switch/mqtt.py rename to homeassistant/components/mqtt/switch.py diff --git a/homeassistant/components/vacuum/mqtt.py b/homeassistant/components/mqtt/vacuum.py similarity index 100% rename from homeassistant/components/vacuum/mqtt.py rename to homeassistant/components/mqtt/vacuum.py diff --git a/tests/components/alarm_control_panel/test_mqtt.py b/tests/components/mqtt/test_alarm_control_panel.py similarity index 100% rename from tests/components/alarm_control_panel/test_mqtt.py rename to tests/components/mqtt/test_alarm_control_panel.py diff --git a/tests/components/binary_sensor/test_mqtt.py b/tests/components/mqtt/test_binary_sensor.py similarity index 100% rename from tests/components/binary_sensor/test_mqtt.py rename to tests/components/mqtt/test_binary_sensor.py diff --git a/tests/components/camera/test_mqtt.py b/tests/components/mqtt/test_camera.py similarity index 100% rename from tests/components/camera/test_mqtt.py rename to tests/components/mqtt/test_camera.py diff --git a/tests/components/climate/test_mqtt.py b/tests/components/mqtt/test_climate.py similarity index 100% rename from tests/components/climate/test_mqtt.py rename to tests/components/mqtt/test_climate.py diff --git a/tests/components/cover/test_mqtt.py b/tests/components/mqtt/test_cover.py similarity index 99% rename from tests/components/cover/test_mqtt.py rename to tests/components/mqtt/test_cover.py index 1616a93d0b5..36f566d0c19 100644 --- a/tests/components/cover/test_mqtt.py +++ b/tests/components/mqtt/test_cover.py @@ -5,7 +5,7 @@ from unittest.mock import ANY from homeassistant.components import cover, mqtt from homeassistant.components.cover import (ATTR_POSITION, ATTR_TILT_POSITION) -from homeassistant.components.cover.mqtt import MqttCover +from homeassistant.components.mqtt.cover import MqttCover from homeassistant.components.mqtt.discovery import async_start from homeassistant.const import ( ATTR_ASSUMED_STATE, ATTR_ENTITY_ID, diff --git a/tests/components/fan/test_mqtt.py b/tests/components/mqtt/test_fan.py similarity index 100% rename from tests/components/fan/test_mqtt.py rename to tests/components/mqtt/test_fan.py diff --git a/tests/components/light/test_mqtt.py b/tests/components/mqtt/test_light.py similarity index 100% rename from tests/components/light/test_mqtt.py rename to tests/components/mqtt/test_light.py diff --git a/tests/components/light/test_mqtt_json.py b/tests/components/mqtt/test_light_json.py similarity index 100% rename from tests/components/light/test_mqtt_json.py rename to tests/components/mqtt/test_light_json.py diff --git a/tests/components/light/test_mqtt_template.py b/tests/components/mqtt/test_light_template.py similarity index 100% rename from tests/components/light/test_mqtt_template.py rename to tests/components/mqtt/test_light_template.py diff --git a/tests/components/lock/test_mqtt.py b/tests/components/mqtt/test_lock.py similarity index 100% rename from tests/components/lock/test_mqtt.py rename to tests/components/mqtt/test_lock.py diff --git a/tests/components/sensor/test_mqtt.py b/tests/components/mqtt/test_sensor.py similarity index 99% rename from tests/components/sensor/test_mqtt.py rename to tests/components/mqtt/test_sensor.py index f5eb8b23cf1..9de76ff64f4 100644 --- a/tests/components/sensor/test_mqtt.py +++ b/tests/components/mqtt/test_sensor.py @@ -268,7 +268,7 @@ class TestSensorMQTT(unittest.TestCase): assert '100' == \ state.attributes.get('val') - @patch('homeassistant.components.sensor.mqtt._LOGGER') + @patch('homeassistant.components.mqtt.sensor._LOGGER') def test_update_with_json_attrs_not_dict(self, mock_logger): """Test attributes get extracted from a JSON result.""" mock_component(self.hass, 'mqtt') @@ -289,7 +289,7 @@ class TestSensorMQTT(unittest.TestCase): assert state.attributes.get('val') is None assert mock_logger.warning.called - @patch('homeassistant.components.sensor.mqtt._LOGGER') + @patch('homeassistant.components.mqtt.sensor._LOGGER') def test_update_with_json_attrs_bad_JSON(self, mock_logger): """Test attributes get extracted from a JSON result.""" mock_component(self.hass, 'mqtt') diff --git a/tests/components/switch/test_mqtt.py b/tests/components/mqtt/test_switch.py similarity index 100% rename from tests/components/switch/test_mqtt.py rename to tests/components/mqtt/test_switch.py diff --git a/tests/components/vacuum/test_mqtt.py b/tests/components/mqtt/test_vacuum.py similarity index 99% rename from tests/components/vacuum/test_mqtt.py rename to tests/components/mqtt/test_vacuum.py index 9031034ff6e..356ce44c6cb 100644 --- a/tests/components/vacuum/test_mqtt.py +++ b/tests/components/mqtt/test_vacuum.py @@ -8,8 +8,9 @@ from homeassistant.const import ( from homeassistant.components import vacuum, mqtt from homeassistant.components.vacuum import ( ATTR_BATTERY_LEVEL, ATTR_BATTERY_ICON, ATTR_STATUS, - ATTR_FAN_SPEED, mqtt as mqttvacuum) -from homeassistant.components.mqtt import CONF_COMMAND_TOPIC + ATTR_FAN_SPEED) +from homeassistant.components.mqtt import ( + CONF_COMMAND_TOPIC, vacuum as mqttvacuum) from homeassistant.components.mqtt.discovery import async_start from tests.common import ( async_mock_mqtt_component, From 336b6adc88d3dc8582332322cbf01e4a6f2ee2e8 Mon Sep 17 00:00:00 2001 From: Adam Mills Date: Tue, 15 Jan 2019 12:33:34 -0500 Subject: [PATCH 177/235] Split time_pattern triggers from time trigger (#19825) * Split interval triggers from time trigger * Default smaller interval units to zero * Rename interval to schedule * Rename schedule to time_pattern --- homeassistant/components/automation/time.py | 22 +- .../components/automation/time_pattern.py | 53 +++++ tests/components/automation/test_time.py | 174 -------------- .../automation/test_time_pattern.py | 219 ++++++++++++++++++ 4 files changed, 277 insertions(+), 191 deletions(-) create mode 100644 homeassistant/components/automation/time_pattern.py create mode 100644 tests/components/automation/test_time_pattern.py diff --git a/homeassistant/components/automation/time.py b/homeassistant/components/automation/time.py index 116bfbdbc97..d57e190490f 100644 --- a/homeassistant/components/automation/time.py +++ b/homeassistant/components/automation/time.py @@ -13,30 +13,18 @@ from homeassistant.const import CONF_AT, CONF_PLATFORM from homeassistant.helpers import config_validation as cv from homeassistant.helpers.event import async_track_time_change -CONF_HOURS = 'hours' -CONF_MINUTES = 'minutes' -CONF_SECONDS = 'seconds' - _LOGGER = logging.getLogger(__name__) -TRIGGER_SCHEMA = vol.All(vol.Schema({ +TRIGGER_SCHEMA = vol.Schema({ vol.Required(CONF_PLATFORM): 'time', - CONF_AT: cv.time, - CONF_HOURS: vol.Any(vol.Coerce(int), vol.Coerce(str)), - CONF_MINUTES: vol.Any(vol.Coerce(int), vol.Coerce(str)), - CONF_SECONDS: vol.Any(vol.Coerce(int), vol.Coerce(str)), -}), cv.has_at_least_one_key(CONF_HOURS, CONF_MINUTES, CONF_SECONDS, CONF_AT)) + vol.Required(CONF_AT): cv.time, +}) async def async_trigger(hass, config, action, automation_info): """Listen for state changes based on configuration.""" - if CONF_AT in config: - at_time = config.get(CONF_AT) - hours, minutes, seconds = at_time.hour, at_time.minute, at_time.second - else: - hours = config.get(CONF_HOURS) - minutes = config.get(CONF_MINUTES) - seconds = config.get(CONF_SECONDS) + at_time = config.get(CONF_AT) + hours, minutes, seconds = at_time.hour, at_time.minute, at_time.second @callback def time_automation_listener(now): diff --git a/homeassistant/components/automation/time_pattern.py b/homeassistant/components/automation/time_pattern.py new file mode 100644 index 00000000000..8b6e907f7b8 --- /dev/null +++ b/homeassistant/components/automation/time_pattern.py @@ -0,0 +1,53 @@ +""" +Offer time listening automation rules. + +For more details about this automation rule, please refer to the documentation +at https://home-assistant.io/docs/automation/trigger/#time-trigger +""" +import logging + +import voluptuous as vol + +from homeassistant.core import callback +from homeassistant.const import CONF_PLATFORM +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.event import async_track_time_change + +CONF_HOURS = 'hours' +CONF_MINUTES = 'minutes' +CONF_SECONDS = 'seconds' + +_LOGGER = logging.getLogger(__name__) + +TRIGGER_SCHEMA = vol.All(vol.Schema({ + vol.Required(CONF_PLATFORM): 'time_pattern', + CONF_HOURS: vol.Any(vol.Coerce(int), vol.Coerce(str)), + CONF_MINUTES: vol.Any(vol.Coerce(int), vol.Coerce(str)), + CONF_SECONDS: vol.Any(vol.Coerce(int), vol.Coerce(str)), +}), cv.has_at_least_one_key(CONF_HOURS, CONF_MINUTES, CONF_SECONDS)) + + +async def async_trigger(hass, config, action, automation_info): + """Listen for state changes based on configuration.""" + hours = config.get(CONF_HOURS) + minutes = config.get(CONF_MINUTES) + seconds = config.get(CONF_SECONDS) + + # If larger units are specified, default the smaller units to zero + if minutes is None and hours is not None: + minutes = 0 + if seconds is None and minutes is not None: + seconds = 0 + + @callback + def time_automation_listener(now): + """Listen for time changes and calls action.""" + hass.async_run_job(action, { + 'trigger': { + 'platform': 'time_pattern', + 'now': now, + }, + }) + + return async_track_time_change(hass, time_automation_listener, + hour=hours, minute=minutes, second=seconds) diff --git a/tests/components/automation/test_time.py b/tests/components/automation/test_time.py index 11387f25889..0e570800342 100644 --- a/tests/components/automation/test_time.py +++ b/tests/components/automation/test_time.py @@ -10,7 +10,6 @@ import homeassistant.components.automation as automation from tests.common import ( async_fire_time_changed, assert_setup_component, mock_component) -from tests.components.automation import common from tests.common import async_mock_service @@ -26,158 +25,6 @@ def setup_comp(hass): mock_component(hass, 'group') -async def test_if_fires_when_hour_matches(hass, calls): - """Test for firing if hour is matching.""" - assert await async_setup_component(hass, automation.DOMAIN, { - automation.DOMAIN: { - 'trigger': { - 'platform': 'time', - 'hours': 0, - }, - 'action': { - 'service': 'test.automation' - } - } - }) - - async_fire_time_changed(hass, dt_util.utcnow().replace(hour=0)) - await hass.async_block_till_done() - assert 1 == len(calls) - - await common.async_turn_off(hass) - await hass.async_block_till_done() - - async_fire_time_changed(hass, dt_util.utcnow().replace(hour=0)) - await hass.async_block_till_done() - assert 1 == len(calls) - - -async def test_if_fires_when_minute_matches(hass, calls): - """Test for firing if minutes are matching.""" - assert await async_setup_component(hass, automation.DOMAIN, { - automation.DOMAIN: { - 'trigger': { - 'platform': 'time', - 'minutes': 0, - }, - 'action': { - 'service': 'test.automation' - } - } - }) - - async_fire_time_changed(hass, dt_util.utcnow().replace(minute=0)) - - await hass.async_block_till_done() - assert 1 == len(calls) - - -async def test_if_fires_when_second_matches(hass, calls): - """Test for firing if seconds are matching.""" - assert await async_setup_component(hass, automation.DOMAIN, { - automation.DOMAIN: { - 'trigger': { - 'platform': 'time', - 'seconds': 0, - }, - 'action': { - 'service': 'test.automation' - } - } - }) - - async_fire_time_changed(hass, dt_util.utcnow().replace(second=0)) - - await hass.async_block_till_done() - assert 1 == len(calls) - - -async def test_if_fires_when_all_matches(hass, calls): - """Test for firing if everything matches.""" - assert await async_setup_component(hass, automation.DOMAIN, { - automation.DOMAIN: { - 'trigger': { - 'platform': 'time', - 'hours': 1, - 'minutes': 2, - 'seconds': 3, - }, - 'action': { - 'service': 'test.automation' - } - } - }) - - async_fire_time_changed(hass, dt_util.utcnow().replace( - hour=1, minute=2, second=3)) - - await hass.async_block_till_done() - assert 1 == len(calls) - - -async def test_if_fires_periodic_seconds(hass, calls): - """Test for firing periodically every second.""" - assert await async_setup_component(hass, automation.DOMAIN, { - automation.DOMAIN: { - 'trigger': { - 'platform': 'time', - 'seconds': "/2", - }, - 'action': { - 'service': 'test.automation' - } - } - }) - - async_fire_time_changed(hass, dt_util.utcnow().replace( - hour=0, minute=0, second=2)) - - await hass.async_block_till_done() - assert 1 == len(calls) - - -async def test_if_fires_periodic_minutes(hass, calls): - """Test for firing periodically every minute.""" - assert await async_setup_component(hass, automation.DOMAIN, { - automation.DOMAIN: { - 'trigger': { - 'platform': 'time', - 'minutes': "/2", - }, - 'action': { - 'service': 'test.automation' - } - } - }) - - async_fire_time_changed(hass, dt_util.utcnow().replace( - hour=0, minute=2, second=0)) - - await hass.async_block_till_done() - assert 1 == len(calls) - - -async def test_if_fires_periodic_hours(hass, calls): - """Test for firing periodically every hour.""" - assert await async_setup_component(hass, automation.DOMAIN, { - automation.DOMAIN: { - 'trigger': { - 'platform': 'time', - 'hours': "/2", - }, - 'action': { - 'service': 'test.automation' - } - } - }) - - async_fire_time_changed(hass, dt_util.utcnow().replace( - hour=2, minute=0, second=0)) - - await hass.async_block_till_done() - assert 1 == len(calls) - - async def test_if_fires_using_at(hass, calls): """Test for firing at.""" assert await async_setup_component(hass, automation.DOMAIN, { @@ -204,27 +51,6 @@ async def test_if_fires_using_at(hass, calls): assert 'time - 5' == calls[0].data['some'] -async def test_if_not_working_if_no_values_in_conf_provided(hass, calls): - """Test for failure if no configuration.""" - with assert_setup_component(0): - assert await async_setup_component(hass, automation.DOMAIN, { - automation.DOMAIN: { - 'trigger': { - 'platform': 'time', - }, - 'action': { - 'service': 'test.automation' - } - } - }) - - async_fire_time_changed(hass, dt_util.utcnow().replace( - hour=5, minute=0, second=0)) - - await hass.async_block_till_done() - assert 0 == len(calls) - - async def test_if_not_fires_using_wrong_at(hass, calls): """YAML translates time values to total seconds. diff --git a/tests/components/automation/test_time_pattern.py b/tests/components/automation/test_time_pattern.py new file mode 100644 index 00000000000..70a3fe308d5 --- /dev/null +++ b/tests/components/automation/test_time_pattern.py @@ -0,0 +1,219 @@ +"""The tests for the time_pattern automation.""" +import pytest + +from homeassistant.setup import async_setup_component +import homeassistant.util.dt as dt_util +import homeassistant.components.automation as automation + +from tests.common import async_fire_time_changed, mock_component +from tests.components.automation import common +from tests.common import async_mock_service + + +@pytest.fixture +def calls(hass): + """Track calls to a mock serivce.""" + return async_mock_service(hass, 'test', 'automation') + + +@pytest.fixture(autouse=True) +def setup_comp(hass): + """Initialize components.""" + mock_component(hass, 'group') + + +async def test_if_fires_when_hour_matches(hass, calls): + """Test for firing if hour is matching.""" + assert await async_setup_component(hass, automation.DOMAIN, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'time_pattern', + 'hours': 0, + 'minutes': '*', + 'seconds': '*', + }, + 'action': { + 'service': 'test.automation' + } + } + }) + + async_fire_time_changed(hass, dt_util.utcnow().replace(hour=0)) + await hass.async_block_till_done() + assert 1 == len(calls) + + await common.async_turn_off(hass) + await hass.async_block_till_done() + + async_fire_time_changed(hass, dt_util.utcnow().replace(hour=0)) + await hass.async_block_till_done() + assert 1 == len(calls) + + +async def test_if_fires_when_minute_matches(hass, calls): + """Test for firing if minutes are matching.""" + assert await async_setup_component(hass, automation.DOMAIN, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'time_pattern', + 'hours': '*', + 'minutes': 0, + 'seconds': '*', + }, + 'action': { + 'service': 'test.automation' + } + } + }) + + async_fire_time_changed(hass, dt_util.utcnow().replace(minute=0)) + + await hass.async_block_till_done() + assert 1 == len(calls) + + +async def test_if_fires_when_second_matches(hass, calls): + """Test for firing if seconds are matching.""" + assert await async_setup_component(hass, automation.DOMAIN, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'time_pattern', + 'hours': '*', + 'minutes': '*', + 'seconds': 0, + }, + 'action': { + 'service': 'test.automation' + } + } + }) + + async_fire_time_changed(hass, dt_util.utcnow().replace(second=0)) + + await hass.async_block_till_done() + assert 1 == len(calls) + + +async def test_if_fires_when_all_matches(hass, calls): + """Test for firing if everything matches.""" + assert await async_setup_component(hass, automation.DOMAIN, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'time_pattern', + 'hours': 1, + 'minutes': 2, + 'seconds': 3, + }, + 'action': { + 'service': 'test.automation' + } + } + }) + + async_fire_time_changed(hass, dt_util.utcnow().replace( + hour=1, minute=2, second=3)) + + await hass.async_block_till_done() + assert 1 == len(calls) + + +async def test_if_fires_periodic_seconds(hass, calls): + """Test for firing periodically every second.""" + assert await async_setup_component(hass, automation.DOMAIN, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'time_pattern', + 'hours': '*', + 'minutes': '*', + 'seconds': "/2", + }, + 'action': { + 'service': 'test.automation' + } + } + }) + + async_fire_time_changed(hass, dt_util.utcnow().replace( + hour=0, minute=0, second=2)) + + await hass.async_block_till_done() + assert 1 == len(calls) + + +async def test_if_fires_periodic_minutes(hass, calls): + """Test for firing periodically every minute.""" + assert await async_setup_component(hass, automation.DOMAIN, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'time_pattern', + 'hours': '*', + 'minutes': "/2", + 'seconds': '*', + }, + 'action': { + 'service': 'test.automation' + } + } + }) + + async_fire_time_changed(hass, dt_util.utcnow().replace( + hour=0, minute=2, second=0)) + + await hass.async_block_till_done() + assert 1 == len(calls) + + +async def test_if_fires_periodic_hours(hass, calls): + """Test for firing periodically every hour.""" + assert await async_setup_component(hass, automation.DOMAIN, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'time_pattern', + 'hours': "/2", + 'minutes': '*', + 'seconds': '*', + }, + 'action': { + 'service': 'test.automation' + } + } + }) + + async_fire_time_changed(hass, dt_util.utcnow().replace( + hour=2, minute=0, second=0)) + + await hass.async_block_till_done() + assert 1 == len(calls) + + +async def test_default_values(hass, calls): + """Test for firing at 2 minutes every hour.""" + assert await async_setup_component(hass, automation.DOMAIN, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'time_pattern', + 'minutes': "2", + }, + 'action': { + 'service': 'test.automation' + } + } + }) + + async_fire_time_changed(hass, dt_util.utcnow().replace( + hour=1, minute=2, second=0)) + + await hass.async_block_till_done() + assert 1 == len(calls) + + async_fire_time_changed(hass, dt_util.utcnow().replace( + hour=1, minute=2, second=1)) + + await hass.async_block_till_done() + assert 1 == len(calls) + + async_fire_time_changed(hass, dt_util.utcnow().replace( + hour=2, minute=2, second=0)) + + await hass.async_block_till_done() + assert 2 == len(calls) From a3f0d5573772307bf57d3cadd4e1723614c435e5 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Tue, 15 Jan 2019 19:29:56 +0100 Subject: [PATCH 178/235] Change deCONZ to embedded platforms (#20113) Move all platforms into components/deconz --- .../{binary_sensor/deconz.py => deconz/binary_sensor.py} | 7 ++++--- .../components/{cover/deconz.py => deconz/cover.py} | 7 ++++--- .../components/{light/deconz.py => deconz/light.py} | 7 ++++--- .../components/{scene/deconz.py => deconz/scene.py} | 0 .../components/{sensor/deconz.py => deconz/sensor.py} | 7 ++++--- .../components/{switch/deconz.py => deconz/switch.py} | 5 +++-- .../test_deconz.py => deconz/test_binary_sensor.py} | 0 .../{cover/test_deconz.py => deconz/test_cover.py} | 0 .../{light/test_deconz.py => deconz/test_light.py} | 0 .../{scene/test_deconz.py => deconz/test_scene.py} | 0 .../{sensor/test_deconz.py => deconz/test_sensor.py} | 0 .../{switch/test_deconz.py => deconz/test_switch.py} | 0 12 files changed, 19 insertions(+), 14 deletions(-) rename homeassistant/components/{binary_sensor/deconz.py => deconz/binary_sensor.py} (98%) rename homeassistant/components/{cover/deconz.py => deconz/cover.py} (99%) rename homeassistant/components/{light/deconz.py => deconz/light.py} (99%) rename homeassistant/components/{scene/deconz.py => deconz/scene.py} (100%) rename homeassistant/components/{sensor/deconz.py => deconz/sensor.py} (99%) rename homeassistant/components/{switch/deconz.py => deconz/switch.py} (98%) rename tests/components/{binary_sensor/test_deconz.py => deconz/test_binary_sensor.py} (100%) rename tests/components/{cover/test_deconz.py => deconz/test_cover.py} (100%) rename tests/components/{light/test_deconz.py => deconz/test_light.py} (100%) rename tests/components/{scene/test_deconz.py => deconz/test_scene.py} (100%) rename tests/components/{sensor/test_deconz.py => deconz/test_sensor.py} (100%) rename tests/components/{switch/test_deconz.py => deconz/test_switch.py} (100%) diff --git a/homeassistant/components/binary_sensor/deconz.py b/homeassistant/components/deconz/binary_sensor.py similarity index 98% rename from homeassistant/components/binary_sensor/deconz.py rename to homeassistant/components/deconz/binary_sensor.py index b9fdb08e068..c67a4673983 100644 --- a/homeassistant/components/binary_sensor/deconz.py +++ b/homeassistant/components/deconz/binary_sensor.py @@ -5,14 +5,15 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/binary_sensor.deconz/ """ from homeassistant.components.binary_sensor import BinarySensorDevice -from homeassistant.components.deconz.const import ( - ATTR_DARK, ATTR_ON, CONF_ALLOW_CLIP_SENSOR, DECONZ_REACHABLE, - DOMAIN as DECONZ_DOMAIN) from homeassistant.const import ATTR_BATTERY_LEVEL from homeassistant.core import callback from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE from homeassistant.helpers.dispatcher import async_dispatcher_connect +from .const import ( + ATTR_DARK, ATTR_ON, CONF_ALLOW_CLIP_SENSOR, DECONZ_REACHABLE, + DOMAIN as DECONZ_DOMAIN) + DEPENDENCIES = ['deconz'] diff --git a/homeassistant/components/cover/deconz.py b/homeassistant/components/deconz/cover.py similarity index 99% rename from homeassistant/components/cover/deconz.py rename to homeassistant/components/deconz/cover.py index be60997869c..ffd5c912a37 100644 --- a/homeassistant/components/cover/deconz.py +++ b/homeassistant/components/deconz/cover.py @@ -4,9 +4,6 @@ Support for deCONZ covers. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/cover.deconz/ """ -from homeassistant.components.deconz.const import ( - COVER_TYPES, DAMPERS, DECONZ_REACHABLE, DOMAIN as DECONZ_DOMAIN, - WINDOW_COVERS) from homeassistant.components.cover import ( ATTR_POSITION, CoverDevice, SUPPORT_CLOSE, SUPPORT_OPEN, SUPPORT_STOP, SUPPORT_SET_POSITION) @@ -14,6 +11,10 @@ from homeassistant.core import callback from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE from homeassistant.helpers.dispatcher import async_dispatcher_connect +from .const import ( + COVER_TYPES, DAMPERS, DECONZ_REACHABLE, DOMAIN as DECONZ_DOMAIN, + WINDOW_COVERS) + DEPENDENCIES = ['deconz'] ZIGBEE_SPEC = ['lumi.curtain'] diff --git a/homeassistant/components/light/deconz.py b/homeassistant/components/deconz/light.py similarity index 99% rename from homeassistant/components/light/deconz.py rename to homeassistant/components/deconz/light.py index ae2d241d81f..a7ad38dd8b7 100644 --- a/homeassistant/components/light/deconz.py +++ b/homeassistant/components/deconz/light.py @@ -4,9 +4,6 @@ Support for deCONZ light. For more details about this component, please refer to the documentation at https://home-assistant.io/components/light.deconz/ """ -from homeassistant.components.deconz.const import ( - CONF_ALLOW_DECONZ_GROUPS, DECONZ_REACHABLE, DOMAIN as DECONZ_DOMAIN, - COVER_TYPES, SWITCH_TYPES) from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_FLASH, ATTR_HS_COLOR, ATTR_TRANSITION, EFFECT_COLORLOOP, FLASH_LONG, FLASH_SHORT, @@ -17,6 +14,10 @@ from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE from homeassistant.helpers.dispatcher import async_dispatcher_connect import homeassistant.util.color as color_util +from .const import ( + CONF_ALLOW_DECONZ_GROUPS, DECONZ_REACHABLE, DOMAIN as DECONZ_DOMAIN, + COVER_TYPES, SWITCH_TYPES) + DEPENDENCIES = ['deconz'] diff --git a/homeassistant/components/scene/deconz.py b/homeassistant/components/deconz/scene.py similarity index 100% rename from homeassistant/components/scene/deconz.py rename to homeassistant/components/deconz/scene.py diff --git a/homeassistant/components/sensor/deconz.py b/homeassistant/components/deconz/sensor.py similarity index 99% rename from homeassistant/components/sensor/deconz.py rename to homeassistant/components/deconz/sensor.py index e2c9b59c59c..da5dcc84935 100644 --- a/homeassistant/components/sensor/deconz.py +++ b/homeassistant/components/deconz/sensor.py @@ -4,9 +4,6 @@ Support for deCONZ sensor. For more details about this component, please refer to the documentation at https://home-assistant.io/components/sensor.deconz/ """ -from homeassistant.components.deconz.const import ( - ATTR_DARK, ATTR_ON, CONF_ALLOW_CLIP_SENSOR, DECONZ_REACHABLE, - DOMAIN as DECONZ_DOMAIN) from homeassistant.const import ( ATTR_BATTERY_LEVEL, ATTR_VOLTAGE, DEVICE_CLASS_BATTERY) from homeassistant.core import callback @@ -15,6 +12,10 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity from homeassistant.util import slugify +from .const import ( + ATTR_DARK, ATTR_ON, CONF_ALLOW_CLIP_SENSOR, DECONZ_REACHABLE, + DOMAIN as DECONZ_DOMAIN) + DEPENDENCIES = ['deconz'] ATTR_CURRENT = 'current' diff --git a/homeassistant/components/switch/deconz.py b/homeassistant/components/deconz/switch.py similarity index 98% rename from homeassistant/components/switch/deconz.py rename to homeassistant/components/deconz/switch.py index b491bc4b567..6c64fce5bd5 100644 --- a/homeassistant/components/switch/deconz.py +++ b/homeassistant/components/deconz/switch.py @@ -4,13 +4,14 @@ Support for deCONZ switches. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/switch.deconz/ """ -from homeassistant.components.deconz.const import ( - DECONZ_REACHABLE, DOMAIN as DECONZ_DOMAIN, POWER_PLUGS, SIRENS) from homeassistant.components.switch import SwitchDevice from homeassistant.core import callback from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE from homeassistant.helpers.dispatcher import async_dispatcher_connect +from .const import ( + DECONZ_REACHABLE, DOMAIN as DECONZ_DOMAIN, POWER_PLUGS, SIRENS) + DEPENDENCIES = ['deconz'] diff --git a/tests/components/binary_sensor/test_deconz.py b/tests/components/deconz/test_binary_sensor.py similarity index 100% rename from tests/components/binary_sensor/test_deconz.py rename to tests/components/deconz/test_binary_sensor.py diff --git a/tests/components/cover/test_deconz.py b/tests/components/deconz/test_cover.py similarity index 100% rename from tests/components/cover/test_deconz.py rename to tests/components/deconz/test_cover.py diff --git a/tests/components/light/test_deconz.py b/tests/components/deconz/test_light.py similarity index 100% rename from tests/components/light/test_deconz.py rename to tests/components/deconz/test_light.py diff --git a/tests/components/scene/test_deconz.py b/tests/components/deconz/test_scene.py similarity index 100% rename from tests/components/scene/test_deconz.py rename to tests/components/deconz/test_scene.py diff --git a/tests/components/sensor/test_deconz.py b/tests/components/deconz/test_sensor.py similarity index 100% rename from tests/components/sensor/test_deconz.py rename to tests/components/deconz/test_sensor.py diff --git a/tests/components/switch/test_deconz.py b/tests/components/deconz/test_switch.py similarity index 100% rename from tests/components/switch/test_deconz.py rename to tests/components/deconz/test_switch.py From 11602c1da07cb0d326e0c12237d9b1afa6a52dbf Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Tue, 15 Jan 2019 20:30:50 +0100 Subject: [PATCH 179/235] Improve Philips Hue color conversion 2 (#20118) * Add gamut capability to color util * Include gamut in hue_test * Improve Philips Hue color conversion * correct import for new location hue.light * include file changes between PR's * update aiohue version * update aiohue version * update aiohue version * fix hue_test Now Idea why it failed compared to the previous time * Include gamut in hue_test * fix hue_test * Try to test hue gamut conversion supply a color that is well outside the color gamut of the light, and see if the response is correctly converted to within the reach of the light. * switch from gamut A to gamut B for the tests. * remove white space in blanck line * Fix gamut hue test * Add Gamut tests for the util.color * fix hue gamut test * fix hue gamut test * Improve Philips Hue color conversion --- homeassistant/components/hue/__init__.py | 2 +- homeassistant/components/hue/light.py | 18 ++- homeassistant/util/color.py | 150 +++++++++++++++++++++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/hue/test_light.py | 138 ++++++++++++++++++--- tests/util/test_color.py | 64 ++++++++++ 7 files changed, 342 insertions(+), 34 deletions(-) diff --git a/homeassistant/components/hue/__init__.py b/homeassistant/components/hue/__init__.py index 9c28d08054b..b10e5bb29de 100644 --- a/homeassistant/components/hue/__init__.py +++ b/homeassistant/components/hue/__init__.py @@ -19,7 +19,7 @@ from .bridge import HueBridge # Loading the config flow file will register the flow from .config_flow import configured_hosts -REQUIREMENTS = ['aiohue==1.5.0'] +REQUIREMENTS = ['aiohue==1.8.0'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/hue/light.py b/homeassistant/components/hue/light.py index 28a2d79de13..7a1449e00c6 100644 --- a/homeassistant/components/hue/light.py +++ b/homeassistant/components/hue/light.py @@ -221,9 +221,16 @@ class HueLight(Light): if is_group: self.is_osram = False self.is_philips = False + self.gamut_typ = 'None' + self.gamut = None else: self.is_osram = light.manufacturername == 'OSRAM' self.is_philips = light.manufacturername == 'Philips' + self.gamut_typ = self.light.colorgamuttype + self.gamut = self.light.colorgamut + if not self.gamut: + err_msg = 'Can not get color gamut of light "%s"' + _LOGGER.warning(err_msg, self.name) @property def unique_id(self): @@ -256,7 +263,7 @@ class HueLight(Light): source = self.light.action if self.is_group else self.light.state if mode in ('xy', 'hs') and 'xy' in source: - return color.color_xy_to_hs(*source['xy']) + return color.color_xy_to_hs(*source['xy'], self.gamut) return None @@ -290,6 +297,11 @@ class HueLight(Light): """Flag supported features.""" return SUPPORT_HUE.get(self.light.type, SUPPORT_HUE_EXTENDED) + @property + def effect(self): + """Return the current effect.""" + return self.light.state.get('effect', None) + @property def effect_list(self): """Return the list of supported effects.""" @@ -331,7 +343,9 @@ class HueLight(Light): # Philips hue bulb models respond differently to hue/sat # requests, so we convert to XY first to ensure a consistent # color. - command['xy'] = color.color_hs_to_xy(*kwargs[ATTR_HS_COLOR]) + xy_color = color.color_hs_to_xy(*kwargs[ATTR_HS_COLOR], + self.gamut) + command['xy'] = xy_color elif ATTR_COLOR_TEMP in kwargs: temp = kwargs[ATTR_COLOR_TEMP] command['ct'] = max(self.min_mireds, min(temp, self.max_mireds)) diff --git a/homeassistant/util/color.py b/homeassistant/util/color.py index 0538bfbf369..5a32d89a793 100644 --- a/homeassistant/util/color.py +++ b/homeassistant/util/color.py @@ -2,7 +2,8 @@ import math import colorsys -from typing import Tuple, List +from typing import Tuple, List, Optional +import attr # Official CSS3 colors from w3.org: # https://www.w3.org/TR/2010/PR-css3-color-20101028/#html4 @@ -162,6 +163,24 @@ COLORS = { } +@attr.s() +class XYPoint: + """Represents a CIE 1931 XY coordinate pair.""" + + x = attr.ib(type=float) + y = attr.ib(type=float) + + +@attr.s() +class GamutType: + """Represents the Gamut of a light.""" + + # ColorGamut = gamut(xypoint(xR,yR),xypoint(xG,yG),xypoint(xB,yB)) + red = attr.ib(type=XYPoint) + green = attr.ib(type=XYPoint) + blue = attr.ib(type=XYPoint) + + def color_name_to_rgb(color_name: str) -> Tuple[int, int, int]: """Convert color name to RGB hex value.""" # COLORS map has no spaces in it, so make the color_name have no @@ -174,9 +193,10 @@ def color_name_to_rgb(color_name: str) -> Tuple[int, int, int]: # pylint: disable=invalid-name -def color_RGB_to_xy(iR: int, iG: int, iB: int) -> Tuple[float, float]: +def color_RGB_to_xy(iR: int, iG: int, iB: int, + Gamut: Optional[GamutType] = None) -> Tuple[float, float]: """Convert from RGB color to XY color.""" - return color_RGB_to_xy_brightness(iR, iG, iB)[:2] + return color_RGB_to_xy_brightness(iR, iG, iB, Gamut)[:2] # Taken from: @@ -184,7 +204,8 @@ def color_RGB_to_xy(iR: int, iG: int, iB: int) -> Tuple[float, float]: # License: Code is given as is. Use at your own risk and discretion. # pylint: disable=invalid-name def color_RGB_to_xy_brightness( - iR: int, iG: int, iB: int) -> Tuple[float, float, int]: + iR: int, iG: int, iB: int, + Gamut: Optional[GamutType] = None) -> Tuple[float, float, int]: """Convert from RGB color to XY color.""" if iR + iG + iB == 0: return 0.0, 0.0, 0 @@ -214,19 +235,36 @@ def color_RGB_to_xy_brightness( Y = 1 if Y > 1 else Y brightness = round(Y * 255) + # Check if the given xy value is within the color-reach of the lamp. + if Gamut: + in_reach = check_point_in_lamps_reach((x, y), Gamut) + if not in_reach: + xy_closest = get_closest_point_to_point((x, y), Gamut) + x = xy_closest[0] + y = xy_closest[1] + return round(x, 3), round(y, 3), brightness -def color_xy_to_RGB(vX: float, vY: float) -> Tuple[int, int, int]: +def color_xy_to_RGB( + vX: float, vY: float, + Gamut: Optional[GamutType] = None) -> Tuple[int, int, int]: """Convert from XY to a normalized RGB.""" - return color_xy_brightness_to_RGB(vX, vY, 255) + return color_xy_brightness_to_RGB(vX, vY, 255, Gamut) # Converted to Python from Obj-C, original source from: # http://www.developers.meethue.com/documentation/color-conversions-rgb-xy -def color_xy_brightness_to_RGB(vX: float, vY: float, - ibrightness: int) -> Tuple[int, int, int]: +def color_xy_brightness_to_RGB( + vX: float, vY: float, ibrightness: int, + Gamut: Optional[GamutType] = None) -> Tuple[int, int, int]: """Convert from XYZ to RGB.""" + if Gamut: + if not check_point_in_lamps_reach((vX, vY), Gamut): + xy_closest = get_closest_point_to_point((vX, vY), Gamut) + vX = xy_closest[0] + vY = xy_closest[1] + brightness = ibrightness / 255. if brightness == 0: return (0, 0, 0) @@ -338,15 +376,17 @@ def color_hs_to_RGB(iH: float, iS: float) -> Tuple[int, int, int]: return color_hsv_to_RGB(iH, iS, 100) -def color_xy_to_hs(vX: float, vY: float) -> Tuple[float, float]: +def color_xy_to_hs(vX: float, vY: float, + Gamut: Optional[GamutType] = None) -> Tuple[float, float]: """Convert an xy color to its hs representation.""" - h, s, _ = color_RGB_to_hsv(*color_xy_to_RGB(vX, vY)) + h, s, _ = color_RGB_to_hsv(*color_xy_to_RGB(vX, vY, Gamut)) return h, s -def color_hs_to_xy(iH: float, iS: float) -> Tuple[float, float]: +def color_hs_to_xy(iH: float, iS: float, + Gamut: Optional[GamutType] = None) -> Tuple[float, float]: """Convert an hs color to its xy representation.""" - return color_RGB_to_xy(*color_hs_to_RGB(iH, iS)) + return color_RGB_to_xy(*color_hs_to_RGB(iH, iS), Gamut) def _match_max_scale(input_colors: Tuple, output_colors: Tuple) -> Tuple: @@ -474,3 +514,89 @@ def color_temperature_mired_to_kelvin(mired_temperature: float) -> float: def color_temperature_kelvin_to_mired(kelvin_temperature: float) -> float: """Convert degrees kelvin to mired shift.""" return math.floor(1000000 / kelvin_temperature) + + +# The following 5 functions are adapted from rgbxy provided by Benjamin Knight +# License: The MIT License (MIT), 2014. +# https://github.com/benknight/hue-python-rgb-converter +def cross_product(p1: XYPoint, p2: XYPoint) -> float: + """Calculate the cross product of two XYPoints.""" + return float(p1.x * p2.y - p1.y * p2.x) + + +def get_distance_between_two_points(one: XYPoint, two: XYPoint) -> float: + """Calculate the distance between two XYPoints.""" + dx = one.x - two.x + dy = one.y - two.y + return math.sqrt(dx * dx + dy * dy) + + +def get_closest_point_to_line(A: XYPoint, B: XYPoint, P: XYPoint) -> XYPoint: + """ + Find the closest point from P to a line defined by A and B. + + This point will be reproducible by the lamp + as it is on the edge of the gamut. + """ + AP = XYPoint(P.x - A.x, P.y - A.y) + AB = XYPoint(B.x - A.x, B.y - A.y) + ab2 = AB.x * AB.x + AB.y * AB.y + ap_ab = AP.x * AB.x + AP.y * AB.y + t = ap_ab / ab2 + + if t < 0.0: + t = 0.0 + elif t > 1.0: + t = 1.0 + + return XYPoint(A.x + AB.x * t, A.y + AB.y * t) + + +def get_closest_point_to_point(xy_tuple: Tuple[float, float], + Gamut: GamutType) -> Tuple[float, float]: + """ + Get the closest matching color within the gamut of the light. + + Should only be used if the supplied color is outside of the color gamut. + """ + xy_point = XYPoint(xy_tuple[0], xy_tuple[1]) + + # find the closest point on each line in the CIE 1931 'triangle'. + pAB = get_closest_point_to_line(Gamut.red, Gamut.green, xy_point) + pAC = get_closest_point_to_line(Gamut.blue, Gamut.red, xy_point) + pBC = get_closest_point_to_line(Gamut.green, Gamut.blue, xy_point) + + # Get the distances per point and see which point is closer to our Point. + dAB = get_distance_between_two_points(xy_point, pAB) + dAC = get_distance_between_two_points(xy_point, pAC) + dBC = get_distance_between_two_points(xy_point, pBC) + + lowest = dAB + closest_point = pAB + + if dAC < lowest: + lowest = dAC + closest_point = pAC + + if dBC < lowest: + lowest = dBC + closest_point = pBC + + # Change the xy value to a value which is within the reach of the lamp. + cx = closest_point.x + cy = closest_point.y + + return (cx, cy) + + +def check_point_in_lamps_reach(p: Tuple[float, float], + Gamut: GamutType) -> bool: + """Check if the provided XYPoint can be recreated by a Hue lamp.""" + v1 = XYPoint(Gamut.green.x - Gamut.red.x, Gamut.green.y - Gamut.red.y) + v2 = XYPoint(Gamut.blue.x - Gamut.red.x, Gamut.blue.y - Gamut.red.y) + + q = XYPoint(p[0] - Gamut.red.x, p[1] - Gamut.red.y) + s = cross_product(q, v2) / cross_product(v1, v2) + t = cross_product(v1, q) / cross_product(v1, v2) + + return (s >= 0.0) and (t >= 0.0) and (s + t <= 1.0) diff --git a/requirements_all.txt b/requirements_all.txt index aed3f5cbb37..001dc4e40f8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -112,7 +112,7 @@ aioharmony==0.1.2 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==1.5.0 +aiohue==1.8.0 # homeassistant.components.sensor.imap aioimaplib==0.7.13 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3b5eabfaf6e..a2d8edb4e9a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -38,7 +38,7 @@ aioautomatic==0.6.5 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==1.5.0 +aiohue==1.8.0 # homeassistant.components.unifi aiounifi==4 diff --git a/tests/components/hue/test_light.py b/tests/components/hue/test_light.py index 023a3416968..f7865fcf4f8 100644 --- a/tests/components/hue/test_light.py +++ b/tests/components/hue/test_light.py @@ -85,6 +85,16 @@ LIGHT_1_ON = { "colormode": "xy", "reachable": True }, + "capabilities": { + "control": { + "colorgamuttype": "A", + "colorgamut": [ + [0.704, 0.296], + [0.2151, 0.7106], + [0.138, 0.08] + ] + } + }, "type": "Extended color light", "name": "Hue Lamp 1", "modelid": "LCT001", @@ -105,6 +115,16 @@ LIGHT_1_OFF = { "colormode": "xy", "reachable": True }, + "capabilities": { + "control": { + "colorgamuttype": "A", + "colorgamut": [ + [0.704, 0.296], + [0.2151, 0.7106], + [0.138, 0.08] + ] + } + }, "type": "Extended color light", "name": "Hue Lamp 1", "modelid": "LCT001", @@ -125,6 +145,16 @@ LIGHT_2_OFF = { "colormode": "hs", "reachable": True }, + "capabilities": { + "control": { + "colorgamuttype": "A", + "colorgamut": [ + [0.704, 0.296], + [0.2151, 0.7106], + [0.138, 0.08] + ] + } + }, "type": "Extended color light", "name": "Hue Lamp 2", "modelid": "LCT001", @@ -145,6 +175,16 @@ LIGHT_2_ON = { "colormode": "hs", "reachable": True }, + "capabilities": { + "control": { + "colorgamuttype": "A", + "colorgamut": [ + [0.704, 0.296], + [0.2151, 0.7106], + [0.138, 0.08] + ] + } + }, "type": "Extended color light", "name": "Hue Lamp 2 new", "modelid": "LCT001", @@ -156,6 +196,23 @@ LIGHT_RESPONSE = { "1": LIGHT_1_ON, "2": LIGHT_2_OFF, } +LIGHT_RAW = { + "capabilities": { + "control": { + "colorgamuttype": "A", + "colorgamut": [ + [0.704, 0.296], + [0.2151, 0.7106], + [0.138, 0.08] + ] + } + }, + "swversion": "66009461", +} +LIGHT_GAMUT = color.GamutType(color.XYPoint(0.704, 0.296), + color.XYPoint(0.2151, 0.7106), + color.XYPoint(0.138, 0.08)) +LIGHT_GAMUT_TYPE = 'A' @pytest.fixture @@ -380,6 +437,16 @@ async def test_new_light_discovered(hass, mock_bridge): "colormode": "hs", "reachable": True }, + "capabilities": { + "control": { + "colorgamuttype": "A", + "colorgamut": [ + [0.704, 0.296], + [0.2151, 0.7106], + [0.138, 0.08] + ] + } + }, "type": "Extended color light", "name": "Hue Lamp 3", "modelid": "LCT001", @@ -493,6 +560,16 @@ async def test_other_light_update(hass, mock_bridge): "colormode": "hs", "reachable": True }, + "capabilities": { + "control": { + "colorgamuttype": "A", + "colorgamut": [ + [0.704, 0.296], + [0.2151, 0.7106], + [0.138, 0.08] + ] + } + }, "type": "Extended color light", "name": "Hue Lamp 2 new", "modelid": "LCT001", @@ -573,6 +650,21 @@ async def test_light_turn_on_service(hass, mock_bridge): assert light is not None assert light.state == 'on' + # test hue gamut in turn_on service + await hass.services.async_call('light', 'turn_on', { + 'entity_id': 'light.hue_lamp_2', + 'rgb_color': [0, 0, 255], + }, blocking=True) + + assert len(mock_bridge.mock_requests) == 5 + + assert mock_bridge.mock_requests[3]['json'] == { + 'on': True, + 'xy': (0.138, 0.08), + 'effect': 'none', + 'alert': 'none', + } + async def test_light_turn_off_service(hass, mock_bridge): """Test calling the turn on service on a light.""" @@ -608,7 +700,8 @@ async def test_light_turn_off_service(hass, mock_bridge): def test_available(): """Test available property.""" light = hue_light.HueLight( - light=Mock(state={'reachable': False}), + light=Mock(state={'reachable': False}, + raw=LIGHT_RAW), request_bridge_update=None, bridge=Mock(allow_unreachable=False), is_group=False, @@ -617,7 +710,8 @@ def test_available(): assert light.available is False light = hue_light.HueLight( - light=Mock(state={'reachable': False}), + light=Mock(state={'reachable': False}, + raw=LIGHT_RAW), request_bridge_update=None, bridge=Mock(allow_unreachable=True), is_group=False, @@ -626,7 +720,8 @@ def test_available(): assert light.available is True light = hue_light.HueLight( - light=Mock(state={'reachable': False}), + light=Mock(state={'reachable': False}, + raw=LIGHT_RAW), request_bridge_update=None, bridge=Mock(allow_unreachable=False), is_group=True, @@ -639,10 +734,13 @@ def test_hs_color(): """Test hs_color property.""" light = hue_light.HueLight( light=Mock(state={ - 'colormode': 'ct', - 'hue': 1234, - 'sat': 123, - }), + 'colormode': 'ct', + 'hue': 1234, + 'sat': 123, + }, + raw=LIGHT_RAW, + colorgamuttype=LIGHT_GAMUT_TYPE, + colorgamut=LIGHT_GAMUT), request_bridge_update=None, bridge=Mock(), is_group=False, @@ -652,10 +750,13 @@ def test_hs_color(): light = hue_light.HueLight( light=Mock(state={ - 'colormode': 'hs', - 'hue': 1234, - 'sat': 123, - }), + 'colormode': 'hs', + 'hue': 1234, + 'sat': 123, + }, + raw=LIGHT_RAW, + colorgamuttype=LIGHT_GAMUT_TYPE, + colorgamut=LIGHT_GAMUT), request_bridge_update=None, bridge=Mock(), is_group=False, @@ -665,14 +766,17 @@ def test_hs_color(): light = hue_light.HueLight( light=Mock(state={ - 'colormode': 'xy', - 'hue': 1234, - 'sat': 123, - 'xy': [0.4, 0.5] - }), + 'colormode': 'xy', + 'hue': 1234, + 'sat': 123, + 'xy': [0.4, 0.5] + }, + raw=LIGHT_RAW, + colorgamuttype=LIGHT_GAMUT_TYPE, + colorgamut=LIGHT_GAMUT), request_bridge_update=None, bridge=Mock(), is_group=False, ) - assert light.hs_color == color.color_xy_to_hs(0.4, 0.5) + assert light.hs_color == color.color_xy_to_hs(0.4, 0.5, LIGHT_GAMUT) diff --git a/tests/util/test_color.py b/tests/util/test_color.py index b7802d3dc09..b54b2bc5776 100644 --- a/tests/util/test_color.py +++ b/tests/util/test_color.py @@ -5,6 +5,10 @@ import homeassistant.util.color as color_util import pytest import voluptuous as vol +GAMUT = color_util.GamutType(color_util.XYPoint(0.704, 0.296), + color_util.XYPoint(0.2151, 0.7106), + color_util.XYPoint(0.138, 0.08)) + class TestColorUtil(unittest.TestCase): """Test color util methods.""" @@ -29,6 +33,15 @@ class TestColorUtil(unittest.TestCase): assert (0.701, 0.299, 16) == \ color_util.color_RGB_to_xy_brightness(128, 0, 0) + assert (0.7, 0.299, 72) == \ + color_util.color_RGB_to_xy_brightness(255, 0, 0, GAMUT) + + assert (0.215, 0.711, 170) == \ + color_util.color_RGB_to_xy_brightness(0, 255, 0, GAMUT) + + assert (0.138, 0.08, 12) == \ + color_util.color_RGB_to_xy_brightness(0, 0, 255, GAMUT) + def test_color_RGB_to_xy(self): """Test color_RGB_to_xy.""" assert (0, 0) == \ @@ -48,6 +61,15 @@ class TestColorUtil(unittest.TestCase): assert (0.701, 0.299) == \ color_util.color_RGB_to_xy(128, 0, 0) + assert (0.138, 0.08) == \ + color_util.color_RGB_to_xy(0, 0, 255, GAMUT) + + assert (0.215, 0.711) == \ + color_util.color_RGB_to_xy(0, 255, 0, GAMUT) + + assert (0.7, 0.299) == \ + color_util.color_RGB_to_xy(255, 0, 0, GAMUT) + def test_color_xy_brightness_to_RGB(self): """Test color_xy_brightness_to_RGB.""" assert (0, 0, 0) == \ @@ -68,6 +90,15 @@ class TestColorUtil(unittest.TestCase): assert (0, 63, 255) == \ color_util.color_xy_brightness_to_RGB(0, 0, 255) + assert (255, 0, 3) == \ + color_util.color_xy_brightness_to_RGB(1, 0, 255, GAMUT) + + assert (82, 255, 0) == \ + color_util.color_xy_brightness_to_RGB(0, 1, 255, GAMUT) + + assert (9, 85, 255) == \ + color_util.color_xy_brightness_to_RGB(0, 0, 255, GAMUT) + def test_color_xy_to_RGB(self): """Test color_xy_to_RGB.""" assert (255, 243, 222) == \ @@ -82,6 +113,15 @@ class TestColorUtil(unittest.TestCase): assert (0, 63, 255) == \ color_util.color_xy_to_RGB(0, 0) + assert (255, 0, 3) == \ + color_util.color_xy_to_RGB(1, 0, GAMUT) + + assert (82, 255, 0) == \ + color_util.color_xy_to_RGB(0, 1, GAMUT) + + assert (9, 85, 255) == \ + color_util.color_xy_to_RGB(0, 0, GAMUT) + def test_color_RGB_to_hsv(self): """Test color_RGB_to_hsv.""" assert (0, 0, 0) == \ @@ -150,6 +190,15 @@ class TestColorUtil(unittest.TestCase): assert (225.176, 100) == \ color_util.color_xy_to_hs(0, 0) + assert (359.294, 100) == \ + color_util.color_xy_to_hs(1, 0, GAMUT) + + assert (100.706, 100) == \ + color_util.color_xy_to_hs(0, 1, GAMUT) + + assert (221.463, 96.471) == \ + color_util.color_xy_to_hs(0, 0, GAMUT) + def test_color_hs_to_xy(self): """Test color_hs_to_xy.""" assert (0.151, 0.343) == \ @@ -167,6 +216,21 @@ class TestColorUtil(unittest.TestCase): assert (0.323, 0.329) == \ color_util.color_hs_to_xy(360, 0) + assert (0.7, 0.299) == \ + color_util.color_hs_to_xy(0, 100, GAMUT) + + assert (0.215, 0.711) == \ + color_util.color_hs_to_xy(120, 100, GAMUT) + + assert (0.17, 0.34) == \ + color_util.color_hs_to_xy(180, 100, GAMUT) + + assert (0.138, 0.08) == \ + color_util.color_hs_to_xy(240, 100, GAMUT) + + assert (0.7, 0.299) == \ + color_util.color_hs_to_xy(360, 100, GAMUT) + def test_rgb_hex_to_rgb_list(self): """Test rgb_hex_to_rgb_list.""" assert [255, 255, 255] == \ From cc166bf6a751ade8fbe075ad2dbb77312bf56b11 Mon Sep 17 00:00:00 2001 From: Morgan Kesler Date: Tue, 15 Jan 2019 17:19:04 -0500 Subject: [PATCH 180/235] Enable setting alarm mode night for arlo platform --- .../components/alarm_control_panel/arlo.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/alarm_control_panel/arlo.py b/homeassistant/components/alarm_control_panel/arlo.py index 8842c710a05..715f6127fbd 100644 --- a/homeassistant/components/alarm_control_panel/arlo.py +++ b/homeassistant/components/alarm_control_panel/arlo.py @@ -17,7 +17,7 @@ from homeassistant.components.arlo import ( DATA_ARLO, CONF_ATTRIBUTION, SIGNAL_UPDATE_ARLO) from homeassistant.const import ( ATTR_ATTRIBUTION, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, - STATE_ALARM_DISARMED) + STATE_ALARM_DISARMED, STATE_ALARM_ARMED_NIGHT) _LOGGER = logging.getLogger(__name__) @@ -25,6 +25,7 @@ ARMED = 'armed' CONF_HOME_MODE_NAME = 'home_mode_name' CONF_AWAY_MODE_NAME = 'away_mode_name' +CONF_NIGHT_MODE_NAME = 'night_mode_name' DEPENDENCIES = ['arlo'] @@ -35,6 +36,7 @@ ICON = 'mdi:security' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_HOME_MODE_NAME, default=ARMED): cv.string, vol.Optional(CONF_AWAY_MODE_NAME, default=ARMED): cv.string, + vol.Optional(CONF_NIGHT_MODE_NAME, default=ARMED): cv.string, }) @@ -47,21 +49,23 @@ def setup_platform(hass, config, add_entities, discovery_info=None): home_mode_name = config.get(CONF_HOME_MODE_NAME) away_mode_name = config.get(CONF_AWAY_MODE_NAME) + night_mode_name = config.get(CONF_NIGHT_MODE_NAME) base_stations = [] for base_station in arlo.base_stations: base_stations.append(ArloBaseStation(base_station, home_mode_name, - away_mode_name)) + away_mode_name, night_mode_name)) add_entities(base_stations, True) class ArloBaseStation(AlarmControlPanel): """Representation of an Arlo Alarm Control Panel.""" - def __init__(self, data, home_mode_name, away_mode_name): + def __init__(self, data, home_mode_name, away_mode_name, night_mode_name): """Initialize the alarm control panel.""" self._base_station = data self._home_mode_name = home_mode_name self._away_mode_name = away_mode_name + self._night_mode_name = night_mode_name self._state = None @property @@ -105,6 +109,10 @@ class ArloBaseStation(AlarmControlPanel): """Send arm home command. Uses custom mode.""" self._base_station.mode = self._home_mode_name + async def async_alarm_arm_night(self, code=None): + """Send arm away command. Uses custom mode.""" + self._base_station.mode = self._night_mode_name + @property def name(self): """Return the name of the base station.""" @@ -128,4 +136,6 @@ class ArloBaseStation(AlarmControlPanel): return STATE_ALARM_ARMED_HOME if mode == self._away_mode_name: return STATE_ALARM_ARMED_AWAY + if mode == self._night_mode_name: + return STATE_ALARM_ARMED_NIGHT return mode From 0ec1401be7700ea6bbe62f78fedcfdd988989fbf Mon Sep 17 00:00:00 2001 From: emontnemery Date: Tue, 15 Jan 2019 23:26:37 +0100 Subject: [PATCH 181/235] Minor refactoring of MQTT availability (#20136) * Small refactor of MQTT availability * Use dict[key] for required config keys and keys with default values. --- homeassistant/components/mqtt/__init__.py | 29 ++++++------------- .../components/mqtt/alarm_control_panel.py | 10 ++----- .../components/mqtt/binary_sensor.py | 13 ++------- homeassistant/components/mqtt/climate.py | 13 ++------- homeassistant/components/mqtt/cover.py | 26 +++++++---------- homeassistant/components/mqtt/fan.py | 12 ++------ .../components/mqtt/light/schema_basic.py | 10 ++----- .../components/mqtt/light/schema_json.py | 10 ++----- .../components/mqtt/light/schema_template.py | 10 ++----- homeassistant/components/mqtt/lock.py | 10 ++----- homeassistant/components/mqtt/sensor.py | 10 ++----- homeassistant/components/mqtt/switch.py | 14 +++------ homeassistant/components/mqtt/vacuum.py | 7 +---- 13 files changed, 45 insertions(+), 129 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 801cb2997d4..dbdb7f7eb32 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -892,17 +892,12 @@ class MqttAttributes(Entity): class MqttAvailability(Entity): """Mixin used for platforms that report availability.""" - def __init__(self, availability_topic: Optional[str], qos: Optional[int], - payload_available: Optional[str], - payload_not_available: Optional[str]) -> None: + def __init__(self, config: dict) -> None: """Initialize the availability mixin.""" self._availability_sub_state = None self._available = False # type: bool - self._availability_topic = availability_topic - self._availability_qos = qos - self._payload_available = payload_available - self._payload_not_available = payload_not_available + self._avail_config = config async def async_added_to_hass(self) -> None: """Subscribe MQTT events. @@ -914,16 +909,9 @@ class MqttAvailability(Entity): async def availability_discovery_update(self, config: dict): """Handle updated discovery message.""" - self._availability_setup_from_config(config) + self._avail_config = config await self._availability_subscribe_topics() - def _availability_setup_from_config(self, config): - """(Re)Setup.""" - self._availability_topic = config.get(CONF_AVAILABILITY_TOPIC) - self._availability_qos = config.get(CONF_QOS) - self._payload_available = config.get(CONF_PAYLOAD_AVAILABLE) - self._payload_not_available = config.get(CONF_PAYLOAD_NOT_AVAILABLE) - async def _availability_subscribe_topics(self): """(Re)Subscribe to topics.""" from .subscription import async_subscribe_topics @@ -933,9 +921,9 @@ class MqttAvailability(Entity): payload: SubscribePayloadType, qos: int) -> None: """Handle a new received MQTT availability message.""" - if payload == self._payload_available: + if payload == self._avail_config[CONF_PAYLOAD_AVAILABLE]: self._available = True - elif payload == self._payload_not_available: + elif payload == self._avail_config[CONF_PAYLOAD_NOT_AVAILABLE]: self._available = False self.async_schedule_update_ha_state() @@ -943,9 +931,9 @@ class MqttAvailability(Entity): self._availability_sub_state = await async_subscribe_topics( self.hass, self._availability_sub_state, {'availability_topic': { - 'topic': self._availability_topic, + 'topic': self._avail_config.get(CONF_AVAILABILITY_TOPIC), 'msg_callback': availability_message_received, - 'qos': self._availability_qos}}) + 'qos': self._avail_config[CONF_QOS]}}) async def async_will_remove_from_hass(self): """Unsubscribe when removed.""" @@ -956,7 +944,8 @@ class MqttAvailability(Entity): @property def available(self) -> bool: """Return if the device is available.""" - return self._availability_topic is None or self._available + availability_topic = self._avail_config.get(CONF_AVAILABILITY_TOPIC) + return availability_topic is None or self._available class MqttDiscoveryUpdate(Entity): diff --git a/homeassistant/components/mqtt/alarm_control_panel.py b/homeassistant/components/mqtt/alarm_control_panel.py index 7d092216a1d..5bd4117ecee 100644 --- a/homeassistant/components/mqtt/alarm_control_panel.py +++ b/homeassistant/components/mqtt/alarm_control_panel.py @@ -17,8 +17,7 @@ from homeassistant.const import ( STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED, STATE_UNKNOWN) from homeassistant.components.mqtt import ( - ATTR_DISCOVERY_HASH, CONF_AVAILABILITY_TOPIC, CONF_COMMAND_TOPIC, - CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, CONF_RETAIN, + ATTR_DISCOVERY_HASH, CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, subscription) from homeassistant.components.mqtt.discovery import ( @@ -95,14 +94,9 @@ class MqttAlarm(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, self._unique_id = config.get(CONF_UNIQUE_ID) self._sub_state = None - availability_topic = config.get(CONF_AVAILABILITY_TOPIC) - payload_available = config.get(CONF_PAYLOAD_AVAILABLE) - payload_not_available = config.get(CONF_PAYLOAD_NOT_AVAILABLE) - qos = config.get(CONF_QOS) device_config = config.get(CONF_DEVICE) - MqttAvailability.__init__(self, availability_topic, qos, - payload_available, payload_not_available) + MqttAvailability.__init__(self, config) MqttDiscoveryUpdate.__init__(self, discovery_hash, self.discovery_update) MqttEntityDeviceInfo.__init__(self, device_config) diff --git a/homeassistant/components/mqtt/binary_sensor.py b/homeassistant/components/mqtt/binary_sensor.py index f42feec6838..95886a46299 100644 --- a/homeassistant/components/mqtt/binary_sensor.py +++ b/homeassistant/components/mqtt/binary_sensor.py @@ -16,10 +16,8 @@ from homeassistant.const import ( CONF_FORCE_UPDATE, CONF_NAME, CONF_VALUE_TEMPLATE, CONF_PAYLOAD_ON, CONF_PAYLOAD_OFF, CONF_DEVICE_CLASS, CONF_DEVICE) from homeassistant.components.mqtt import ( - ATTR_DISCOVERY_HASH, CONF_AVAILABILITY_TOPIC, CONF_STATE_TOPIC, - CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, - MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, - MqttEntityDeviceInfo, subscription) + ATTR_DISCOVERY_HASH, CONF_QOS, CONF_STATE_TOPIC, MqttAttributes, + MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, subscription) from homeassistant.components.mqtt.discovery import ( MQTT_DISCOVERY_NEW, clear_discovery_hash) import homeassistant.helpers.config_validation as cv @@ -96,15 +94,10 @@ class MqttBinarySensor(MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, self._sub_state = None self._delay_listener = None - availability_topic = config.get(CONF_AVAILABILITY_TOPIC) - payload_available = config.get(CONF_PAYLOAD_AVAILABLE) - payload_not_available = config.get(CONF_PAYLOAD_NOT_AVAILABLE) - qos = config.get(CONF_QOS) device_config = config.get(CONF_DEVICE) MqttAttributes.__init__(self, config) - MqttAvailability.__init__(self, availability_topic, qos, - payload_available, payload_not_available) + MqttAvailability.__init__(self, config) MqttDiscoveryUpdate.__init__(self, discovery_hash, self.discovery_update) MqttEntityDeviceInfo.__init__(self, device_config) diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py index 3219b71c1bd..71950f9b1b7 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -21,10 +21,8 @@ from homeassistant.const import ( ATTR_TEMPERATURE, CONF_DEVICE, CONF_NAME, CONF_VALUE_TEMPLATE, STATE_ON, STATE_OFF) from homeassistant.components.mqtt import ( - ATTR_DISCOVERY_HASH, CONF_AVAILABILITY_TOPIC, CONF_QOS, CONF_RETAIN, - CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, - MQTT_BASE_PLATFORM_SCHEMA, MqttAvailability, MqttDiscoveryUpdate, - MqttEntityDeviceInfo, subscription) + ATTR_DISCOVERY_HASH, CONF_QOS, CONF_RETAIN, MQTT_BASE_PLATFORM_SCHEMA, + MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, subscription) from homeassistant.components.mqtt.discovery import ( MQTT_DISCOVERY_NEW, clear_discovery_hash) import homeassistant.helpers.config_validation as cv @@ -210,14 +208,9 @@ class MqttClimate(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, self._setup_from_config(config) - availability_topic = config.get(CONF_AVAILABILITY_TOPIC) - payload_available = config.get(CONF_PAYLOAD_AVAILABLE) - payload_not_available = config.get(CONF_PAYLOAD_NOT_AVAILABLE) - qos = config.get(CONF_QOS) device_config = config.get(CONF_DEVICE) - MqttAvailability.__init__(self, availability_topic, qos, - payload_available, payload_not_available) + MqttAvailability.__init__(self, config) MqttDiscoveryUpdate.__init__(self, discovery_hash, self.discovery_update) MqttEntityDeviceInfo.__init__(self, device_config) diff --git a/homeassistant/components/mqtt/cover.py b/homeassistant/components/mqtt/cover.py index f5001c9c495..5ebe51a3bce 100644 --- a/homeassistant/components/mqtt/cover.py +++ b/homeassistant/components/mqtt/cover.py @@ -20,10 +20,9 @@ from homeassistant.const import ( CONF_NAME, CONF_VALUE_TEMPLATE, CONF_OPTIMISTIC, STATE_OPEN, STATE_CLOSED, STATE_UNKNOWN, CONF_DEVICE) from homeassistant.components.mqtt import ( - ATTR_DISCOVERY_HASH, CONF_AVAILABILITY_TOPIC, CONF_STATE_TOPIC, - CONF_COMMAND_TOPIC, CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, - CONF_QOS, CONF_RETAIN, valid_publish_topic, valid_subscribe_topic, - MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, subscription) + ATTR_DISCOVERY_HASH, CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN, + CONF_STATE_TOPIC, MqttAvailability, MqttDiscoveryUpdate, + MqttEntityDeviceInfo, subscription) from homeassistant.components.mqtt.discovery import ( MQTT_DISCOVERY_NEW, clear_discovery_hash) import homeassistant.helpers.config_validation as cv @@ -92,12 +91,12 @@ def validate_options(value): PLATFORM_SCHEMA = vol.All(mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_COMMAND_TOPIC): valid_publish_topic, - vol.Optional(CONF_SET_POSITION_TOPIC): valid_publish_topic, + vol.Optional(CONF_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_SET_POSITION_TOPIC): mqtt.valid_publish_topic, vol.Optional(CONF_SET_POSITION_TEMPLATE): cv.template, vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean, - vol.Optional(CONF_GET_POSITION_TOPIC): valid_subscribe_topic, - vol.Optional(CONF_STATE_TOPIC): valid_subscribe_topic, + vol.Optional(CONF_GET_POSITION_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_STATE_TOPIC): mqtt.valid_subscribe_topic, vol.Optional(CONF_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_PAYLOAD_OPEN, default=DEFAULT_PAYLOAD_OPEN): cv.string, @@ -110,8 +109,8 @@ PLATFORM_SCHEMA = vol.All(mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({ vol.Optional(CONF_POSITION_CLOSED, default=DEFAULT_POSITION_CLOSED): int, vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, - vol.Optional(CONF_TILT_COMMAND_TOPIC): valid_publish_topic, - vol.Optional(CONF_TILT_STATUS_TOPIC): valid_subscribe_topic, + vol.Optional(CONF_TILT_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_TILT_STATUS_TOPIC): mqtt.valid_subscribe_topic, vol.Optional(CONF_TILT_CLOSED_POSITION, default=DEFAULT_TILT_CLOSED_POSITION): int, vol.Optional(CONF_TILT_OPEN_POSITION, @@ -175,14 +174,9 @@ class MqttCover(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, # Load config self._setup_from_config(config) - availability_topic = config.get(CONF_AVAILABILITY_TOPIC) - payload_available = config.get(CONF_PAYLOAD_AVAILABLE) - payload_not_available = config.get(CONF_PAYLOAD_NOT_AVAILABLE) - qos = config.get(CONF_QOS) device_config = config.get(CONF_DEVICE) - MqttAvailability.__init__(self, availability_topic, qos, - payload_available, payload_not_available) + MqttAvailability.__init__(self, config) MqttDiscoveryUpdate.__init__(self, discovery_hash, self.discovery_update) MqttEntityDeviceInfo.__init__(self, device_config) diff --git a/homeassistant/components/mqtt/fan.py b/homeassistant/components/mqtt/fan.py index 3adb821eba5..22f89a40e04 100644 --- a/homeassistant/components/mqtt/fan.py +++ b/homeassistant/components/mqtt/fan.py @@ -14,9 +14,8 @@ from homeassistant.const import ( CONF_NAME, CONF_OPTIMISTIC, CONF_STATE, STATE_ON, STATE_OFF, CONF_PAYLOAD_OFF, CONF_PAYLOAD_ON, CONF_DEVICE) from homeassistant.components.mqtt import ( - ATTR_DISCOVERY_HASH, CONF_AVAILABILITY_TOPIC, CONF_STATE_TOPIC, - CONF_COMMAND_TOPIC, CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, - CONF_QOS, CONF_RETAIN, MqttAvailability, MqttDiscoveryUpdate, + ATTR_DISCOVERY_HASH, CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN, + CONF_STATE_TOPIC, MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, subscription) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -141,14 +140,9 @@ class MqttFan(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, # Load config self._setup_from_config(config) - availability_topic = config.get(CONF_AVAILABILITY_TOPIC) - payload_available = config.get(CONF_PAYLOAD_AVAILABLE) - payload_not_available = config.get(CONF_PAYLOAD_NOT_AVAILABLE) - qos = config.get(CONF_QOS) device_config = config.get(CONF_DEVICE) - MqttAvailability.__init__(self, availability_topic, qos, - payload_available, payload_not_available) + MqttAvailability.__init__(self, config) MqttDiscoveryUpdate.__init__(self, discovery_hash, self.discovery_update) MqttEntityDeviceInfo.__init__(self, device_config) diff --git a/homeassistant/components/mqtt/light/schema_basic.py b/homeassistant/components/mqtt/light/schema_basic.py index 351eb4c464c..fdfc1961db3 100644 --- a/homeassistant/components/mqtt/light/schema_basic.py +++ b/homeassistant/components/mqtt/light/schema_basic.py @@ -19,8 +19,7 @@ from homeassistant.const import ( CONF_NAME, 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, + CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, subscription) from homeassistant.helpers.restore_state import RestoreEntity import homeassistant.helpers.config_validation as cv @@ -151,14 +150,9 @@ class MqttLight(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, # Load config self._setup_from_config(config) - availability_topic = config.get(CONF_AVAILABILITY_TOPIC) - payload_available = config.get(CONF_PAYLOAD_AVAILABLE) - payload_not_available = config.get(CONF_PAYLOAD_NOT_AVAILABLE) - qos = config.get(CONF_QOS) device_config = config.get(CONF_DEVICE) - MqttAvailability.__init__(self, availability_topic, qos, - payload_available, payload_not_available) + MqttAvailability.__init__(self, config) MqttDiscoveryUpdate.__init__(self, discovery_hash, self.discovery_update) MqttEntityDeviceInfo.__init__(self, device_config) diff --git a/homeassistant/components/mqtt/light/schema_json.py b/homeassistant/components/mqtt/light/schema_json.py index f0888726d78..6c986cbf49f 100644 --- a/homeassistant/components/mqtt/light/schema_json.py +++ b/homeassistant/components/mqtt/light/schema_json.py @@ -16,8 +16,7 @@ from homeassistant.components.light import ( SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, SUPPORT_FLASH, SUPPORT_TRANSITION, SUPPORT_WHITE_VALUE, Light) 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, + CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, subscription) from homeassistant.const import ( CONF_BRIGHTNESS, CONF_COLOR_TEMP, CONF_DEVICE, CONF_EFFECT, CONF_NAME, @@ -114,14 +113,9 @@ class MqttLightJson(MqttAvailability, MqttDiscoveryUpdate, # Load config self._setup_from_config(config) - availability_topic = config.get(CONF_AVAILABILITY_TOPIC) - payload_available = config.get(CONF_PAYLOAD_AVAILABLE) - payload_not_available = config.get(CONF_PAYLOAD_NOT_AVAILABLE) - qos = config.get(CONF_QOS) device_config = config.get(CONF_DEVICE) - MqttAvailability.__init__(self, availability_topic, qos, - payload_available, payload_not_available) + MqttAvailability.__init__(self, config) MqttDiscoveryUpdate.__init__(self, discovery_hash, self.discovery_update) MqttEntityDeviceInfo.__init__(self, device_config) diff --git a/homeassistant/components/mqtt/light/schema_template.py b/homeassistant/components/mqtt/light/schema_template.py index 9f04e9a6468..53423679050 100644 --- a/homeassistant/components/mqtt/light/schema_template.py +++ b/homeassistant/components/mqtt/light/schema_template.py @@ -17,8 +17,7 @@ from homeassistant.components.light import ( from homeassistant.const import ( CONF_DEVICE, CONF_NAME, CONF_OPTIMISTIC, STATE_ON, STATE_OFF) from homeassistant.components.mqtt import ( - CONF_AVAILABILITY_TOPIC, CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, - CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, CONF_RETAIN, + CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN, MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, subscription) import homeassistant.helpers.config_validation as cv import homeassistant.util.color as color_util @@ -101,14 +100,9 @@ class MqttTemplate(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, # Load config self._setup_from_config(config) - availability_topic = config.get(CONF_AVAILABILITY_TOPIC) - payload_available = config.get(CONF_PAYLOAD_AVAILABLE) - payload_not_available = config.get(CONF_PAYLOAD_NOT_AVAILABLE) - qos = config.get(CONF_QOS) device_config = config.get(CONF_DEVICE) - MqttAvailability.__init__(self, availability_topic, qos, - payload_available, payload_not_available) + MqttAvailability.__init__(self, config) MqttDiscoveryUpdate.__init__(self, discovery_hash, self.discovery_update) MqttEntityDeviceInfo.__init__(self, device_config) diff --git a/homeassistant/components/mqtt/lock.py b/homeassistant/components/mqtt/lock.py index 53bfe6ff7a1..e82498a9b12 100644 --- a/homeassistant/components/mqtt/lock.py +++ b/homeassistant/components/mqtt/lock.py @@ -11,8 +11,7 @@ import voluptuous as vol from homeassistant.core import callback from homeassistant.components.lock import LockDevice from homeassistant.components.mqtt import ( - ATTR_DISCOVERY_HASH, CONF_AVAILABILITY_TOPIC, CONF_COMMAND_TOPIC, - CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, CONF_RETAIN, + ATTR_DISCOVERY_HASH, CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, subscription) from homeassistant.const import ( @@ -91,14 +90,9 @@ class MqttLock(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, self._sub_state = None self._optimistic = False - availability_topic = config.get(CONF_AVAILABILITY_TOPIC) - payload_available = config[CONF_PAYLOAD_AVAILABLE] - payload_not_available = config[CONF_PAYLOAD_NOT_AVAILABLE] - qos = config[CONF_QOS] device_config = config.get(CONF_DEVICE) - MqttAvailability.__init__(self, availability_topic, qos, - payload_available, payload_not_available) + MqttAvailability.__init__(self, config) MqttDiscoveryUpdate.__init__(self, discovery_hash, self.discovery_update) MqttEntityDeviceInfo.__init__(self, device_config) diff --git a/homeassistant/components/mqtt/sensor.py b/homeassistant/components/mqtt/sensor.py index b78ebb048ad..688352b1ef6 100644 --- a/homeassistant/components/mqtt/sensor.py +++ b/homeassistant/components/mqtt/sensor.py @@ -14,8 +14,7 @@ import voluptuous as vol from homeassistant.core import callback from homeassistant.components import sensor from homeassistant.components.mqtt import ( - ATTR_DISCOVERY_HASH, CONF_AVAILABILITY_TOPIC, CONF_PAYLOAD_AVAILABLE, - CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, CONF_STATE_TOPIC, MqttAttributes, + ATTR_DISCOVERY_HASH, CONF_QOS, CONF_STATE_TOPIC, MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, subscription) from homeassistant.components.mqtt.discovery import ( MQTT_DISCOVERY_NEW, clear_discovery_hash) @@ -101,10 +100,6 @@ class MqttSensor(MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, self._expiration_trigger = None self._attributes = None - availability_topic = config.get(CONF_AVAILABILITY_TOPIC) - payload_available = config.get(CONF_PAYLOAD_AVAILABLE) - payload_not_available = config.get(CONF_PAYLOAD_NOT_AVAILABLE) - qos = config.get(CONF_QOS) device_config = config.get(CONF_DEVICE) if config.get(CONF_JSON_ATTRS): @@ -112,8 +107,7 @@ class MqttSensor(MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, 'deprecated, replace with "json_attributes_topic"') MqttAttributes.__init__(self, config) - MqttAvailability.__init__(self, availability_topic, qos, - payload_available, payload_not_available) + MqttAvailability.__init__(self, config) MqttDiscoveryUpdate.__init__(self, discovery_hash, self.discovery_update) MqttEntityDeviceInfo.__init__(self, device_config) diff --git a/homeassistant/components/mqtt/switch.py b/homeassistant/components/mqtt/switch.py index 02f0a60aed2..bc8eac86a6d 100644 --- a/homeassistant/components/mqtt/switch.py +++ b/homeassistant/components/mqtt/switch.py @@ -10,10 +10,9 @@ import voluptuous as vol from homeassistant.core import callback from homeassistant.components.mqtt import ( - ATTR_DISCOVERY_HASH, CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, - CONF_AVAILABILITY_TOPIC, CONF_PAYLOAD_AVAILABLE, - CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, CONF_RETAIN, MqttAvailability, - MqttDiscoveryUpdate, MqttEntityDeviceInfo, subscription) + ATTR_DISCOVERY_HASH, CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN, + CONF_STATE_TOPIC, MqttAvailability, MqttDiscoveryUpdate, + MqttEntityDeviceInfo, subscription) from homeassistant.components.mqtt.discovery import ( MQTT_DISCOVERY_NEW, clear_discovery_hash) from homeassistant.components.switch import SwitchDevice @@ -101,14 +100,9 @@ class MqttSwitch(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, # Load config self._setup_from_config(config) - availability_topic = config.get(CONF_AVAILABILITY_TOPIC) - payload_available = config.get(CONF_PAYLOAD_AVAILABLE) - payload_not_available = config.get(CONF_PAYLOAD_NOT_AVAILABLE) - qos = config.get(CONF_QOS) device_config = config.get(CONF_DEVICE) - MqttAvailability.__init__(self, availability_topic, qos, - payload_available, payload_not_available) + MqttAvailability.__init__(self, config) MqttDiscoveryUpdate.__init__(self, discovery_hash, self.discovery_update) MqttEntityDeviceInfo.__init__(self, device_config) diff --git a/homeassistant/components/mqtt/vacuum.py b/homeassistant/components/mqtt/vacuum.py index bc48362b511..612737c990d 100644 --- a/homeassistant/components/mqtt/vacuum.py +++ b/homeassistant/components/mqtt/vacuum.py @@ -196,14 +196,9 @@ class MqttVacuum(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, # Load config self._setup_from_config(config) - qos = config.get(mqtt.CONF_QOS) - availability_topic = config.get(mqtt.CONF_AVAILABILITY_TOPIC) - payload_available = config.get(mqtt.CONF_PAYLOAD_AVAILABLE) - payload_not_available = config.get(mqtt.CONF_PAYLOAD_NOT_AVAILABLE) device_config = config.get(CONF_DEVICE) - MqttAvailability.__init__(self, availability_topic, qos, - payload_available, payload_not_available) + MqttAvailability.__init__(self, config) MqttDiscoveryUpdate.__init__(self, discovery_info, self.discovery_update) MqttEntityDeviceInfo.__init__(self, device_config) From 732743aeb553e5657c163d80d3202ac5b952ecd3 Mon Sep 17 00:00:00 2001 From: Morgan Kesler Date: Tue, 15 Jan 2019 17:27:56 -0500 Subject: [PATCH 182/235] Missed one small comment --- homeassistant/components/alarm_control_panel/arlo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/alarm_control_panel/arlo.py b/homeassistant/components/alarm_control_panel/arlo.py index 715f6127fbd..66f11fab83f 100644 --- a/homeassistant/components/alarm_control_panel/arlo.py +++ b/homeassistant/components/alarm_control_panel/arlo.py @@ -110,7 +110,7 @@ class ArloBaseStation(AlarmControlPanel): self._base_station.mode = self._home_mode_name async def async_alarm_arm_night(self, code=None): - """Send arm away command. Uses custom mode.""" + """Send arm night command. Uses custom mode.""" self._base_station.mode = self._night_mode_name @property From 1b79872dd62486dc16c2c9aa61eee9ab1c4ccfdf Mon Sep 17 00:00:00 2001 From: Tommy Jonsson Date: Wed, 16 Jan 2019 00:31:57 +0100 Subject: [PATCH 183/235] Add notify.html5_dismiss service (#19912) * Add notify.html5_dismiss service * fix test * add can_dismiss * fix service data payload * fix hasattr -> getattr * fixes * move dismiss service to html5 * fix services.yaml * fix line to long --- homeassistant/components/notify/html5.py | 69 ++++++++++++++++--- homeassistant/components/notify/services.yaml | 10 +++ tests/components/notify/test_html5.py | 34 +++++++++ 3 files changed, 105 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/notify/html5.py b/homeassistant/components/notify/html5.py index f70a9cb73c1..6a486bb6362 100644 --- a/homeassistant/components/notify/html5.py +++ b/homeassistant/components/notify/html5.py @@ -7,6 +7,7 @@ https://home-assistant.io/components/notify.html5/ import datetime import json import logging +from functools import partial import time import uuid @@ -20,7 +21,7 @@ from homeassistant.components.frontend import add_manifest_json_key from homeassistant.components.http import HomeAssistantView from homeassistant.components.notify import ( ATTR_DATA, ATTR_TITLE, ATTR_TARGET, PLATFORM_SCHEMA, ATTR_TITLE_DEFAULT, - BaseNotificationService) + BaseNotificationService, DOMAIN) from homeassistant.const import ( URL_ROOT, HTTP_BAD_REQUEST, HTTP_UNAUTHORIZED, HTTP_INTERNAL_SERVER_ERROR) from homeassistant.helpers import config_validation as cv @@ -34,6 +35,8 @@ _LOGGER = logging.getLogger(__name__) REGISTRATIONS_FILE = 'html5_push_registrations.conf' +SERVICE_DISMISS = 'html5_dismiss' + ATTR_GCM_SENDER_ID = 'gcm_sender_id' ATTR_GCM_API_KEY = 'gcm_api_key' @@ -57,6 +60,7 @@ ATTR_ACTION = 'action' ATTR_ACTIONS = 'actions' ATTR_TYPE = 'type' ATTR_URL = 'url' +ATTR_DISMISS = 'dismiss' ATTR_JWT = 'jwt' @@ -80,6 +84,11 @@ SUBSCRIPTION_SCHEMA = vol.All( }) ) +DISMISS_SERVICE_SCHEMA = vol.Schema({ + vol.Optional(ATTR_TARGET): vol.All(cv.ensure_list, [cv.string]), + vol.Optional(ATTR_DATA): dict, +}) + REGISTER_SCHEMA = vol.Schema({ vol.Required(ATTR_SUBSCRIPTION): SUBSCRIPTION_SCHEMA, vol.Required(ATTR_BROWSER): vol.In(['chrome', 'firefox']), @@ -122,7 +131,8 @@ def get_service(hass, config, discovery_info=None): add_manifest_json_key( ATTR_GCM_SENDER_ID, config.get(ATTR_GCM_SENDER_ID)) - return HTML5NotificationService(gcm_api_key, registrations, json_path) + return HTML5NotificationService( + hass, gcm_api_key, registrations, json_path) def _load_config(filename): @@ -326,12 +336,29 @@ class HTML5PushCallbackView(HomeAssistantView): class HTML5NotificationService(BaseNotificationService): """Implement the notification service for HTML5.""" - def __init__(self, gcm_key, registrations, json_path): + def __init__(self, hass, gcm_key, registrations, json_path): """Initialize the service.""" self._gcm_key = gcm_key self.registrations = registrations self.registrations_json_path = json_path + async def async_dismiss_message(service): + """Handle dismissing notification message service calls.""" + kwargs = {} + + if self.targets is not None: + kwargs[ATTR_TARGET] = self.targets + elif service.data.get(ATTR_TARGET) is not None: + kwargs[ATTR_TARGET] = service.data.get(ATTR_TARGET) + + kwargs[ATTR_DATA] = service.data.get(ATTR_DATA) + + await self.async_dismiss(**kwargs) + + hass.services.async_register( + DOMAIN, SERVICE_DISMISS, async_dismiss_message, + schema=DISMISS_SERVICE_SCHEMA) + @property def targets(self): """Return a dictionary of registered targets.""" @@ -340,12 +367,28 @@ class HTML5NotificationService(BaseNotificationService): targets[registration] = registration return targets + def dismiss(self, **kwargs): + """Dismisses a notification.""" + data = kwargs.get(ATTR_DATA) + tag = data.get(ATTR_TAG) if data else "" + payload = { + ATTR_TAG: tag, + ATTR_DISMISS: True, + ATTR_DATA: {} + } + + self._push_message(payload, **kwargs) + + async def async_dismiss(self, **kwargs): + """Dismisses a notification. + + This method must be run in the event loop. + """ + await self.hass.async_add_executor_job( + partial(self.dismiss, **kwargs)) + def send_message(self, message="", **kwargs): """Send a message to a user.""" - import jwt - from pywebpush import WebPusher - - timestamp = int(time.time()) tag = str(uuid.uuid4()) payload = { @@ -354,7 +397,6 @@ class HTML5NotificationService(BaseNotificationService): ATTR_DATA: {}, 'icon': '/static/icons/favicon-192x192.png', ATTR_TAG: tag, - 'timestamp': (timestamp*1000), # Javascript ms since epoch ATTR_TITLE: kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT) } @@ -378,6 +420,17 @@ class HTML5NotificationService(BaseNotificationService): payload.get(ATTR_ACTIONS) is None): payload[ATTR_DATA][ATTR_URL] = URL_ROOT + self._push_message(payload, **kwargs) + + def _push_message(self, payload, **kwargs): + """Send the message.""" + import jwt + from pywebpush import WebPusher + + timestamp = int(time.time()) + + payload['timestamp'] = (timestamp*1000) # Javascript ms since epoch + targets = kwargs.get(ATTR_TARGET) if not targets: diff --git a/homeassistant/components/notify/services.yaml b/homeassistant/components/notify/services.yaml index 23b1c968c4a..1b7944cc7da 100644 --- a/homeassistant/components/notify/services.yaml +++ b/homeassistant/components/notify/services.yaml @@ -16,6 +16,16 @@ notify: description: Extended information for notification. Optional depending on the platform. example: platform specific +html5_dismiss: + description: Dismiss a html5 notification. + fields: + target: + description: An array of targets. Optional. + example: ['my_phone', 'my_tablet'] + data: + description: Extended information of notification. Supports tag. Optional. + example: '{ "tag": "tagname" }' + apns_register: description: Registers a device to receive push notifications. fields: diff --git a/tests/components/notify/test_html5.py b/tests/components/notify/test_html5.py index 6aeba650a8c..e33c297b166 100644 --- a/tests/components/notify/test_html5.py +++ b/tests/components/notify/test_html5.py @@ -81,6 +81,40 @@ class TestHtml5Notify: assert service is not None + @patch('pywebpush.WebPusher') + def test_dismissing_message(self, mock_wp): + """Test dismissing message.""" + hass = MagicMock() + + data = { + 'device': SUBSCRIPTION_1 + } + + m = mock_open(read_data=json.dumps(data)) + with patch( + 'homeassistant.util.json.open', + m, create=True + ): + service = html5.get_service(hass, {'gcm_sender_id': '100'}) + + assert service is not None + + service.dismiss(target=['device', 'non_existing'], + data={'tag': 'test'}) + + assert len(mock_wp.mock_calls) == 3 + + # WebPusher constructor + assert mock_wp.mock_calls[0][1][0] == SUBSCRIPTION_1['subscription'] + # Third mock_call checks the status_code of the response. + assert mock_wp.mock_calls[2][0] == '().send().status_code.__eq__' + + # Call to send + payload = json.loads(mock_wp.mock_calls[1][1][0]) + + assert payload['dismiss'] is True + assert payload['tag'] == 'test' + @patch('pywebpush.WebPusher') def test_sending_message(self, mock_wp): """Test sending message.""" From cd6679eb5ba11e43c150c0b2ca8982f38a2034b4 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 15 Jan 2019 15:35:18 -0800 Subject: [PATCH 184/235] Updated frontend to 20190115.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 03b984eeaef..0c2299074d4 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -24,7 +24,7 @@ from homeassistant.core import callback from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass -REQUIREMENTS = ['home-assistant-frontend==20190113.0'] +REQUIREMENTS = ['home-assistant-frontend==20190115.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log', diff --git a/requirements_all.txt b/requirements_all.txt index 001dc4e40f8..5331e95ceee 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -514,7 +514,7 @@ hole==0.3.0 holidays==0.9.9 # homeassistant.components.frontend -home-assistant-frontend==20190113.0 +home-assistant-frontend==20190115.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a2d8edb4e9a..471c3979013 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -107,7 +107,7 @@ hdate==0.8.7 holidays==0.9.9 # homeassistant.components.frontend -home-assistant-frontend==20190113.0 +home-assistant-frontend==20190115.0 # homeassistant.components.homematicip_cloud homematicip==0.10.3 From 9fd21d20aec2c8a4dc8710808a89f8e739c40e69 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 15 Jan 2019 16:06:04 -0800 Subject: [PATCH 185/235] Fix loading translations for embedded platforms (#20122) * Fix loading translations for embedded platforms * Update doc string * Lint --- homeassistant/helpers/translation.py | 57 ++++++++++++++----- tests/helpers/test_translation.py | 10 +++- .../test_embedded/__init__.py | 6 ++ .../custom_components/test_embedded/switch.py | 7 +++ 4 files changed, 64 insertions(+), 16 deletions(-) create mode 100644 tests/testing_config/custom_components/test_embedded/__init__.py create mode 100644 tests/testing_config/custom_components/test_embedded/switch.py diff --git a/homeassistant/helpers/translation.py b/homeassistant/helpers/translation.py index 2ca621154a1..e1c5895b89a 100644 --- a/homeassistant/helpers/translation.py +++ b/homeassistant/helpers/translation.py @@ -1,10 +1,10 @@ """Translation string lookup helpers.""" import logging -from os import path +import pathlib from typing import Any, Dict, Iterable from homeassistant import config_entries -from homeassistant.loader import get_component, bind_hass +from homeassistant.loader import get_component, get_platform, bind_hass from homeassistant.util.json import load_json from .typing import HomeAssistantType @@ -32,24 +32,51 @@ def flatten(data: Dict) -> Dict[str, Any]: def component_translation_file(hass: HomeAssistantType, component: str, language: str) -> str: - """Return the translation json file location for a component.""" - if '.' in component: - name = component.split('.', 1)[1] - else: - name = component + """Return the translation json file location for a component. - module = get_component(hass, component) + For component one of: + - components/light/.translations/nl.json + - components/.translations/group.nl.json + + For platform one of: + - components/light/.translations/hue.nl.json + - components/hue/.translations/light.nl.json + """ + is_platform = '.' in component + + if not is_platform: + module = get_component(hass, component) + assert module is not None + + module_path = pathlib.Path(module.__file__) + + if module.__name__ == module.__package__: + # light/__init__.py + filename = '{}.json'.format(language) + else: + # group.py + filename = '{}.{}.json'.format(component, language) + + return str(module_path.parent / '.translations' / filename) + + # It's a platform + parts = component.split('.', 1) + module = get_platform(hass, *parts) assert module is not None - component_path = path.dirname(module.__file__) - # If loading translations for the package root, (__init__.py), the - # prefix should be skipped. - if module.__name__ == module.__package__: - filename = '{}.json'.format(language) + # Either within HA or custom_components + # Either light/hue.py or hue/light.py + module_path = pathlib.Path(module.__file__) + + # Compare to parent so we don't have to strip off `.py` + if module_path.parent.name == parts[0]: + # this is light/hue.py + filename = "{}.{}.json".format(parts[1], language) else: - filename = '{}.{}.json'.format(name, language) + # this is hue/light.py + filename = "{}.{}.json".format(parts[0], language) - return path.join(component_path, '.translations', filename) + return str(module_path.parent / '.translations' / filename) def load_translations_files(translation_files: Dict[str, str]) \ diff --git a/tests/helpers/test_translation.py b/tests/helpers/test_translation.py index 99c6f7dddf1..9d62f204dcd 100644 --- a/tests/helpers/test_translation.py +++ b/tests/helpers/test_translation.py @@ -40,7 +40,10 @@ def test_flatten(): async def test_component_translation_file(hass): """Test the component translation file function.""" assert await async_setup_component(hass, 'switch', { - 'switch': {'platform': 'test'} + 'switch': [ + {'platform': 'test'}, + {'platform': 'test_embedded'} + ] }) assert await async_setup_component(hass, 'test_standalone', { 'test_standalone' @@ -53,6 +56,11 @@ async def test_component_translation_file(hass): hass, 'switch.test', 'en')) == path.normpath(hass.config.path( 'custom_components', 'switch', '.translations', 'test.en.json')) + assert path.normpath(translation.component_translation_file( + hass, 'switch.test_embedded', 'en')) == path.normpath(hass.config.path( + 'custom_components', 'test_embedded', '.translations', + 'switch.en.json')) + assert path.normpath(translation.component_translation_file( hass, 'test_standalone', 'en')) == path.normpath(hass.config.path( 'custom_components', '.translations', 'test_standalone.en.json')) diff --git a/tests/testing_config/custom_components/test_embedded/__init__.py b/tests/testing_config/custom_components/test_embedded/__init__.py new file mode 100644 index 00000000000..2206054356e --- /dev/null +++ b/tests/testing_config/custom_components/test_embedded/__init__.py @@ -0,0 +1,6 @@ +"""Component with embedded platforms.""" + + +async def async_setup(hass, config): + """Mock config.""" + return True diff --git a/tests/testing_config/custom_components/test_embedded/switch.py b/tests/testing_config/custom_components/test_embedded/switch.py new file mode 100644 index 00000000000..e4e0f5fcd39 --- /dev/null +++ b/tests/testing_config/custom_components/test_embedded/switch.py @@ -0,0 +1,7 @@ +"""Switch platform for the embedded component.""" + + +async def async_setup_platform(hass, config, async_add_entities_callback, + discovery_info=None): + """Find and return test switches.""" + pass From f36755e4471c1ec38a3f1e341aaa8b2ae7363386 Mon Sep 17 00:00:00 2001 From: Rohan Kapoor Date: Tue, 15 Jan 2019 16:11:30 -0800 Subject: [PATCH 186/235] Switch geofency tests to using an unauthenticated HTTP client (#20080) --- tests/components/geofency/test_init.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/components/geofency/test_init.py b/tests/components/geofency/test_init.py index fa1829e2d68..dbad7ba668b 100644 --- a/tests/components/geofency/test_init.py +++ b/tests/components/geofency/test_init.py @@ -112,8 +112,8 @@ def mock_dev_track(mock_device_tracker_conf): @pytest.fixture -def geofency_client(loop, hass, hass_client): - """Geofency mock client.""" +def geofency_client(loop, hass, aiohttp_client): + """Geofency mock client (unauthenticated).""" assert loop.run_until_complete(async_setup_component( hass, 'persistent_notification', {})) @@ -126,7 +126,7 @@ def geofency_client(loop, hass, hass_client): loop.run_until_complete(hass.async_block_till_done()) with patch('homeassistant.components.device_tracker.update_config'): - yield loop.run_until_complete(hass_client()) + yield loop.run_until_complete(aiohttp_client(hass.http.app)) @pytest.fixture(autouse=True) @@ -146,7 +146,7 @@ def setup_zones(loop, hass): async def webhook_id(hass, geofency_client): """Initialize the Geofency component and get the webhook_id.""" hass.config.api = Mock(base_url='http://example.com') - result = await hass.config_entries.flow.async_init('geofency', context={ + result = await hass.config_entries.flow.async_init(DOMAIN, context={ 'source': 'user' }) assert result['type'] == data_entry_flow.RESULT_TYPE_FORM, result From 4b3d4b275e85fdc58896bdd338a58f176a2728c7 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Tue, 15 Jan 2019 19:12:23 -0500 Subject: [PATCH 187/235] Zha light.turn_on service fixes. (#20085) Set color only if light supports color mode. Set color temp only light supports color temp. Update entity's brightness only if Zigbee command to set the brightness was sent successfuly. --- homeassistant/components/zha/light.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zha/light.py b/homeassistant/components/zha/light.py index 5b06e8fa321..c1a875e465a 100644 --- a/homeassistant/components/zha/light.py +++ b/homeassistant/components/zha/light.py @@ -155,7 +155,8 @@ class Light(ZhaEntity, light.Light): duration = kwargs.get(light.ATTR_TRANSITION, DEFAULT_DURATION) duration = duration * 10 # tenths of s - if light.ATTR_COLOR_TEMP in kwargs: + if light.ATTR_COLOR_TEMP in kwargs and \ + self.supported_features & light.SUPPORT_COLOR_TEMP: temperature = kwargs[light.ATTR_COLOR_TEMP] try: res = await self._endpoint.light_color.move_to_color_temp( @@ -168,7 +169,8 @@ class Light(ZhaEntity, light.Light): return self._color_temp = temperature - if light.ATTR_HS_COLOR in kwargs: + if light.ATTR_HS_COLOR in kwargs and \ + self.supported_features & light.SUPPORT_COLOR: self._hs_color = kwargs[light.ATTR_HS_COLOR] xy_color = color_util.color_hs_to_xy(*self._hs_color) try: @@ -187,7 +189,6 @@ class Light(ZhaEntity, light.Light): if self._brightness is not None: brightness = kwargs.get( light.ATTR_BRIGHTNESS, self._brightness or 255) - self._brightness = brightness # Move to level with on/off: try: res = await self._endpoint.level.move_to_level_with_on_off( @@ -201,6 +202,7 @@ class Light(ZhaEntity, light.Light): self.entity_id, ex) return self._state = 1 + self._brightness = brightness self.async_schedule_update_ha_state() return From 25f630281328c512bc649788fa1c175d6f0b67fa Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Wed, 16 Jan 2019 01:18:57 +0100 Subject: [PATCH 188/235] Switch to ipapi.co (fixes #19846) (#19886) * Switch to ipapi.co (fixes #19846) * Fix name * Update name --- homeassistant/util/location.py | 18 ++++---- tests/fixtures/freegeoip.io.json | 13 ------ tests/fixtures/ipapi.co.json | 20 +++++++++ tests/util/test_location.py | 72 +++++++++++++++----------------- 4 files changed, 63 insertions(+), 60 deletions(-) delete mode 100644 tests/fixtures/freegeoip.io.json create mode 100644 tests/fixtures/ipapi.co.json diff --git a/homeassistant/util/location.py b/homeassistant/util/location.py index 16aec2ec617..f77d1752d6a 100644 --- a/homeassistant/util/location.py +++ b/homeassistant/util/location.py @@ -10,8 +10,8 @@ from typing import Any, Optional, Tuple, Dict import requests ELEVATION_URL = 'http://maps.googleapis.com/maps/api/elevation/json' -FREEGEO_API = 'https://freegeoip.net/json/' IP_API = 'http://ip-api.com/json' +IPAPI = 'https://ipapi.co/json/' # Constants from https://github.com/maurycyp/vincenty # Earth ellipsoid according to WGS 84 @@ -35,7 +35,7 @@ LocationInfo = collections.namedtuple( def detect_location_info() -> Optional[LocationInfo]: """Detect location information.""" - data = _get_freegeoip() + data = _get_ipapi() if data is None: data = _get_ip_api() @@ -159,22 +159,22 @@ def vincenty(point1: Tuple[float, float], point2: Tuple[float, float], return round(s, 6) -def _get_freegeoip() -> Optional[Dict[str, Any]]: - """Query freegeoip.io for location data.""" +def _get_ipapi() -> Optional[Dict[str, Any]]: + """Query ipapi.co for location data.""" try: - raw_info = requests.get(FREEGEO_API, timeout=5).json() + raw_info = requests.get(IPAPI, timeout=5).json() except (requests.RequestException, ValueError): return None return { 'ip': raw_info.get('ip'), - 'country_code': raw_info.get('country_code'), + 'country_code': raw_info.get('country'), 'country_name': raw_info.get('country_name'), 'region_code': raw_info.get('region_code'), - 'region_name': raw_info.get('region_name'), + 'region_name': raw_info.get('region'), 'city': raw_info.get('city'), - 'zip_code': raw_info.get('zip_code'), - 'time_zone': raw_info.get('time_zone'), + 'zip_code': raw_info.get('postal'), + 'time_zone': raw_info.get('timezone'), 'latitude': raw_info.get('latitude'), 'longitude': raw_info.get('longitude'), } diff --git a/tests/fixtures/freegeoip.io.json b/tests/fixtures/freegeoip.io.json deleted file mode 100644 index 8afdaba070e..00000000000 --- a/tests/fixtures/freegeoip.io.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "ip": "1.2.3.4", - "country_code": "US", - "country_name": "United States", - "region_code": "CA", - "region_name": "California", - "city": "San Diego", - "zip_code": "92122", - "time_zone": "America\/Los_Angeles", - "latitude": 32.8594, - "longitude": -117.2073, - "metro_code": 825 -} diff --git a/tests/fixtures/ipapi.co.json b/tests/fixtures/ipapi.co.json new file mode 100644 index 00000000000..f1dc58a756b --- /dev/null +++ b/tests/fixtures/ipapi.co.json @@ -0,0 +1,20 @@ +{ + "ip": "1.2.3.4", + "city": "Bern", + "region": "Bern", + "region_code": "BE", + "country": "CH", + "country_name": "Switzerland", + "continent_code": "EU", + "in_eu": false, + "postal": "3000", + "latitude": 46.9480278, + "longitude": 7.4490812, + "timezone": "Europe/Zurich", + "utc_offset": "+0100", + "country_calling_code": "+41", + "currency": "CHF", + "languages": "de-CH,fr-CH,it-CH,rm", + "asn": "AS6830", + "org": "Liberty Global B.V." +} \ No newline at end of file diff --git a/tests/util/test_location.py b/tests/util/test_location.py index d83979affd0..7d0df7edb18 100644 --- a/tests/util/test_location.py +++ b/tests/util/test_location.py @@ -30,62 +30,59 @@ class TestLocationUtil(TestCase): def test_get_distance_to_same_place(self): """Test getting the distance.""" - meters = location_util.distance(COORDINATES_PARIS[0], - COORDINATES_PARIS[1], - COORDINATES_PARIS[0], - COORDINATES_PARIS[1]) + meters = location_util.distance( + COORDINATES_PARIS[0], COORDINATES_PARIS[1], + COORDINATES_PARIS[0], COORDINATES_PARIS[1]) assert meters == 0 def test_get_distance(self): """Test getting the distance.""" - meters = location_util.distance(COORDINATES_PARIS[0], - COORDINATES_PARIS[1], - COORDINATES_NEW_YORK[0], - COORDINATES_NEW_YORK[1]) + meters = location_util.distance( + COORDINATES_PARIS[0], COORDINATES_PARIS[1], + COORDINATES_NEW_YORK[0], COORDINATES_NEW_YORK[1]) assert meters/1000 - DISTANCE_KM < 0.01 def test_get_kilometers(self): """Test getting the distance between given coordinates in km.""" - kilometers = location_util.vincenty(COORDINATES_PARIS, - COORDINATES_NEW_YORK) + kilometers = location_util.vincenty( + COORDINATES_PARIS, COORDINATES_NEW_YORK) assert round(kilometers, 2) == DISTANCE_KM def test_get_miles(self): """Test getting the distance between given coordinates in miles.""" - miles = location_util.vincenty(COORDINATES_PARIS, - COORDINATES_NEW_YORK, - miles=True) + miles = location_util.vincenty( + COORDINATES_PARIS, COORDINATES_NEW_YORK, miles=True) assert round(miles, 2) == DISTANCE_MILES @requests_mock.Mocker() - def test_detect_location_info_freegeoip(self, m): - """Test detect location info using freegeoip.""" - m.get(location_util.FREEGEO_API, - text=load_fixture('freegeoip.io.json')) + def test_detect_location_info_ipapi(self, m): + """Test detect location info using ipapi.co.""" + m.get( + location_util.IPAPI, text=load_fixture('ipapi.co.json')) info = location_util.detect_location_info(_test_real=True) assert info is not None assert info.ip == '1.2.3.4' - assert info.country_code == 'US' - assert info.country_name == 'United States' - assert info.region_code == 'CA' - assert info.region_name == 'California' - assert info.city == 'San Diego' - assert info.zip_code == '92122' - assert info.time_zone == 'America/Los_Angeles' - assert info.latitude == 32.8594 - assert info.longitude == -117.2073 - assert not info.use_metric + assert info.country_code == 'CH' + assert info.country_name == 'Switzerland' + assert info.region_code == 'BE' + assert info.region_name == 'Bern' + assert info.city == 'Bern' + assert info.zip_code == '3000' + assert info.time_zone == 'Europe/Zurich' + assert info.latitude == 46.9480278 + assert info.longitude == 7.4490812 + assert info.use_metric @requests_mock.Mocker() - @patch('homeassistant.util.location._get_freegeoip', return_value=None) - def test_detect_location_info_ipapi(self, mock_req, mock_freegeoip): - """Test detect location info using freegeoip.""" - mock_req.get(location_util.IP_API, - text=load_fixture('ip-api.com.json')) + @patch('homeassistant.util.location._get_ipapi', return_value=None) + def test_detect_location_info_ip_api(self, mock_req, mock_ipapi): + """Test detect location info using ip-api.com.""" + mock_req.get( + location_util.IP_API, text=load_fixture('ip-api.com.json')) info = location_util.detect_location_info(_test_real=True) @@ -103,11 +100,10 @@ class TestLocationUtil(TestCase): assert not info.use_metric @patch('homeassistant.util.location.elevation', return_value=0) - @patch('homeassistant.util.location._get_freegeoip', return_value=None) + @patch('homeassistant.util.location._get_ipapi', return_value=None) @patch('homeassistant.util.location._get_ip_api', return_value=None) - def test_detect_location_info_both_queries_fail(self, mock_ipapi, - mock_freegeoip, - mock_elevation): + def test_detect_location_info_both_queries_fail( + self, mock_ipapi, mock_ip_api, mock_elevation): """Ensure we return None if both queries fail.""" info = location_util.detect_location_info(_test_real=True) assert info is None @@ -115,8 +111,8 @@ class TestLocationUtil(TestCase): @patch('homeassistant.util.location.requests.get', side_effect=requests.RequestException) def test_freegeoip_query_raises(self, mock_get): - """Test freegeoip query when the request to API fails.""" - info = location_util._get_freegeoip() + """Test ipapi.co query when the request to API fails.""" + info = location_util._get_ipapi() assert info is None @patch('homeassistant.util.location.requests.get', From c218757336fff82b8916ff91849f96b9d25b04f7 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Wed, 16 Jan 2019 01:20:51 +0100 Subject: [PATCH 189/235] Accept both domains and entities in influxdb include (#19927) * Accept both domains and entities in influxdb include * Explicit test * Remove lint --- homeassistant/components/influxdb.py | 5 +- tests/components/test_influxdb.py | 71 ++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/influxdb.py b/homeassistant/components/influxdb.py index dfb41ddf617..2b9a5d9e193 100644 --- a/homeassistant/components/influxdb.py +++ b/homeassistant/components/influxdb.py @@ -154,8 +154,9 @@ def setup(hass, config): return try: - if (whitelist_e and state.entity_id not in whitelist_e) or \ - (whitelist_d and state.domain not in whitelist_d): + if ((whitelist_e or whitelist_d) + and state.entity_id not in whitelist_e + and state.domain not in whitelist_d): return _include_state = _include_value = False diff --git a/tests/components/test_influxdb.py b/tests/components/test_influxdb.py index d74ec41b749..b69bc03760b 100644 --- a/tests/components/test_influxdb.py +++ b/tests/components/test_influxdb.py @@ -394,6 +394,77 @@ class TestInfluxDB(unittest.TestCase): assert not mock_client.return_value.write_points.called mock_client.return_value.write_points.reset_mock() + def test_event_listener_whitelist_domain_and_entities(self, mock_client): + """Test the event listener against a whitelist.""" + config = { + 'influxdb': { + 'host': 'host', + 'username': 'user', + 'password': 'pass', + 'include': { + 'domains': ['fake'], + 'entities': ['other.one'], + } + } + } + assert setup_component(self.hass, influxdb.DOMAIN, config) + self.handler_method = self.hass.bus.listen.call_args_list[0][0][1] + mock_client.return_value.write_points.reset_mock() + + for domain in ('fake', 'another_fake'): + state = mock.MagicMock( + state=1, domain=domain, + entity_id='{}.something'.format(domain), + object_id='something', attributes={}) + event = mock.MagicMock(data={'new_state': state}, time_fired=12345) + body = [{ + 'measurement': '{}.something'.format(domain), + 'tags': { + 'domain': domain, + 'entity_id': 'something', + }, + 'time': 12345, + 'fields': { + 'value': 1, + }, + }] + self.handler_method(event) + self.hass.data[influxdb.DOMAIN].block_till_done() + if domain == 'fake': + assert mock_client.return_value.write_points.call_count == 1 + assert mock_client.return_value.write_points.call_args == \ + mock.call(body) + else: + assert not mock_client.return_value.write_points.called + mock_client.return_value.write_points.reset_mock() + + for entity_id in ('one', 'two'): + state = mock.MagicMock( + state=1, domain='other', + entity_id='other.{}'.format(entity_id), + object_id=entity_id, attributes={}) + event = mock.MagicMock(data={'new_state': state}, time_fired=12345) + body = [{ + 'measurement': 'other.{}'.format(entity_id), + 'tags': { + 'domain': 'other', + 'entity_id': entity_id, + }, + 'time': 12345, + 'fields': { + 'value': 1, + }, + }] + self.handler_method(event) + self.hass.data[influxdb.DOMAIN].block_till_done() + if entity_id == 'one': + assert mock_client.return_value.write_points.call_count == 1 + assert mock_client.return_value.write_points.call_args == \ + mock.call(body) + else: + assert not mock_client.return_value.write_points.called + mock_client.return_value.write_points.reset_mock() + def test_event_listener_invalid_type(self, mock_client): """Test the event listener when an attribute has an invalid type.""" self._setup(mock_client) From 1e784b4d7a7bd585a8a4bc1d3825eb834ef6dff8 Mon Sep 17 00:00:00 2001 From: Rohan Kapoor Date: Tue, 15 Jan 2019 22:26:42 -0800 Subject: [PATCH 190/235] #20043: Remove superfluous dict in WEBHOOK_SCHEMA validation --- homeassistant/components/locative/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/locative/__init__.py b/homeassistant/components/locative/__init__.py index b238f6ee659..1cc47270ba3 100644 --- a/homeassistant/components/locative/__init__.py +++ b/homeassistant/components/locative/__init__.py @@ -44,7 +44,6 @@ def _validate_test_mode(obj: Dict) -> Dict: WEBHOOK_SCHEMA = vol.All( - dict, vol.Schema({ vol.Required(ATTR_LATITUDE): cv.latitude, vol.Required(ATTR_LONGITUDE): cv.longitude, From 19e19009ccd2d3e6a7f877ed0d7a47851270abd7 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 16 Jan 2019 08:33:04 +0100 Subject: [PATCH 191/235] Make all deCONZ platforms use a common base (#20137) * Make all deconz platforms use a common base * Fix one cover to device replacement too much... --- .../components/deconz/binary_sensor.py | 85 ++-------- homeassistant/components/deconz/cover.py | 89 ++-------- .../components/deconz/deconz_device.py | 74 +++++++++ homeassistant/components/deconz/light.py | 99 +++-------- homeassistant/components/deconz/sensor.py | 157 +++--------------- homeassistant/components/deconz/switch.py | 93 ++--------- 6 files changed, 158 insertions(+), 439 deletions(-) create mode 100644 homeassistant/components/deconz/deconz_device.py diff --git a/homeassistant/components/deconz/binary_sensor.py b/homeassistant/components/deconz/binary_sensor.py index c67a4673983..286b310c1a9 100644 --- a/homeassistant/components/deconz/binary_sensor.py +++ b/homeassistant/components/deconz/binary_sensor.py @@ -7,12 +7,11 @@ https://home-assistant.io/components/binary_sensor.deconz/ from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.const import ATTR_BATTERY_LEVEL from homeassistant.core import callback -from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE from homeassistant.helpers.dispatcher import async_dispatcher_connect from .const import ( - ATTR_DARK, ATTR_ON, CONF_ALLOW_CLIP_SENSOR, DECONZ_REACHABLE, - DOMAIN as DECONZ_DOMAIN) + ATTR_DARK, ATTR_ON, CONF_ALLOW_CLIP_SENSOR, DOMAIN as DECONZ_DOMAIN) +from .deconz_device import DeconzDevice DEPENDENCIES = ['deconz'] @@ -45,28 +44,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_sensor(gateway.api.sensors.values()) -class DeconzBinarySensor(BinarySensorDevice): - """Representation of a binary sensor.""" - - def __init__(self, sensor, gateway): - """Set up sensor and add update callback to get data from websocket.""" - self._sensor = sensor - self.gateway = gateway - self.unsub_dispatcher = None - - async def async_added_to_hass(self): - """Subscribe sensors events.""" - self._sensor.register_async_callback(self.async_update_callback) - self.gateway.deconz_ids[self.entity_id] = self._sensor.deconz_id - self.unsub_dispatcher = async_dispatcher_connect( - self.hass, DECONZ_REACHABLE, self.async_update_callback) - - async def async_will_remove_from_hass(self) -> None: - """Disconnect sensor object when removed.""" - if self.unsub_dispatcher is not None: - self.unsub_dispatcher() - self._sensor.remove_callback(self.async_update_callback) - self._sensor = None +class DeconzBinarySensor(DeconzDevice, BinarySensorDevice): + """Representation of a deCONZ binary sensor.""" @callback def async_update_callback(self, reason): @@ -84,65 +63,27 @@ class DeconzBinarySensor(BinarySensorDevice): @property def is_on(self): """Return true if sensor is on.""" - return self._sensor.is_tripped - - @property - def name(self): - """Return the name of the sensor.""" - return self._sensor.name - - @property - def unique_id(self): - """Return a unique identifier for this sensor.""" - return self._sensor.uniqueid + return self._device.is_tripped @property def device_class(self): """Return the class of the sensor.""" - return self._sensor.sensor_class + return self._device.sensor_class @property def icon(self): """Return the icon to use in the frontend.""" - return self._sensor.sensor_icon - - @property - def available(self): - """Return True if sensor is available.""" - return self.gateway.available and self._sensor.reachable - - @property - def should_poll(self): - """No polling needed.""" - return False + return self._device.sensor_icon @property def device_state_attributes(self): """Return the state attributes of the sensor.""" from pydeconz.sensor import PRESENCE attr = {} - if self._sensor.battery: - attr[ATTR_BATTERY_LEVEL] = self._sensor.battery - if self._sensor.on is not None: - attr[ATTR_ON] = self._sensor.on - if self._sensor.type in PRESENCE and self._sensor.dark is not None: - attr[ATTR_DARK] = self._sensor.dark + if self._device.battery: + attr[ATTR_BATTERY_LEVEL] = self._device.battery + if self._device.on is not None: + attr[ATTR_ON] = self._device.on + if self._device.type in PRESENCE and self._device.dark is not None: + attr[ATTR_DARK] = self._device.dark return attr - - @property - def device_info(self): - """Return a device description for device registry.""" - if (self._sensor.uniqueid is None or - self._sensor.uniqueid.count(':') != 7): - return None - serial = self._sensor.uniqueid.split('-', 1)[0] - bridgeid = self.gateway.api.config.bridgeid - return { - 'connections': {(CONNECTION_ZIGBEE, serial)}, - 'identifiers': {(DECONZ_DOMAIN, serial)}, - 'manufacturer': self._sensor.manufacturer, - 'model': self._sensor.modelid, - 'name': self._sensor.name, - 'sw_version': self._sensor.swversion, - 'via_hub': (DECONZ_DOMAIN, bridgeid), - } diff --git a/homeassistant/components/deconz/cover.py b/homeassistant/components/deconz/cover.py index ffd5c912a37..99bdd20a295 100644 --- a/homeassistant/components/deconz/cover.py +++ b/homeassistant/components/deconz/cover.py @@ -8,12 +8,10 @@ from homeassistant.components.cover import ( ATTR_POSITION, CoverDevice, SUPPORT_CLOSE, SUPPORT_OPEN, SUPPORT_STOP, SUPPORT_SET_POSITION) from homeassistant.core import callback -from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE from homeassistant.helpers.dispatcher import async_dispatcher_connect -from .const import ( - COVER_TYPES, DAMPERS, DECONZ_REACHABLE, DOMAIN as DECONZ_DOMAIN, - WINDOW_COVERS) +from .const import COVER_TYPES, DAMPERS, DOMAIN as DECONZ_DOMAIN, WINDOW_COVERS +from .deconz_device import DeconzDevice DEPENDENCIES = ['deconz'] @@ -51,67 +49,36 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_cover(gateway.api.lights.values()) -class DeconzCover(CoverDevice): +class DeconzCover(DeconzDevice, CoverDevice): """Representation of a deCONZ cover.""" - def __init__(self, cover, gateway): + def __init__(self, device, gateway): """Set up cover and add update callback to get data from websocket.""" - self._cover = cover - self.gateway = gateway - self.unsub_dispatcher = None + super().__init__(device, gateway) self._features = SUPPORT_OPEN self._features |= SUPPORT_CLOSE self._features |= SUPPORT_STOP self._features |= SUPPORT_SET_POSITION - async def async_added_to_hass(self): - """Subscribe to covers events.""" - self._cover.register_async_callback(self.async_update_callback) - self.gateway.deconz_ids[self.entity_id] = self._cover.deconz_id - self.unsub_dispatcher = async_dispatcher_connect( - self.hass, DECONZ_REACHABLE, self.async_update_callback) - - async def async_will_remove_from_hass(self) -> None: - """Disconnect cover object when removed.""" - if self.unsub_dispatcher is not None: - self.unsub_dispatcher() - self._cover.remove_callback(self.async_update_callback) - self._cover = None - - @callback - def async_update_callback(self, reason): - """Update the cover's state.""" - self.async_schedule_update_ha_state() - @property def current_cover_position(self): """Return the current position of the cover.""" if self.is_closed: return 0 - return int(self._cover.brightness / 255 * 100) + return int(self._device.brightness / 255 * 100) @property def is_closed(self): """Return if the cover is closed.""" - return not self._cover.state - - @property - def name(self): - """Return the name of the cover.""" - return self._cover.name - - @property - def unique_id(self): - """Return a unique identifier for this cover.""" - return self._cover.uniqueid + return not self._device.state @property def device_class(self): """Return the class of the cover.""" - if self._cover.type in DAMPERS: + if self._device.type in DAMPERS: return 'damper' - if self._cover.type in WINDOW_COVERS: + if self._device.type in WINDOW_COVERS: return 'window' @property @@ -119,16 +86,6 @@ class DeconzCover(CoverDevice): """Flag supported features.""" return self._features - @property - def available(self): - """Return True if light is available.""" - return self.gateway.available and self._cover.reachable - - @property - def should_poll(self): - """No polling needed.""" - return False - async def async_set_cover_position(self, **kwargs): """Move the cover to a specific position.""" position = kwargs[ATTR_POSITION] @@ -136,7 +93,7 @@ class DeconzCover(CoverDevice): if position > 0: data['on'] = True data['bri'] = int(position / 100 * 255) - await self._cover.async_set_state(data) + await self._device.async_set_state(data) async def async_open_cover(self, **kwargs): """Open cover.""" @@ -151,25 +108,7 @@ class DeconzCover(CoverDevice): async def async_stop_cover(self, **kwargs): """Stop cover.""" data = {'bri_inc': 0} - await self._cover.async_set_state(data) - - @property - def device_info(self): - """Return a device description for device registry.""" - if (self._cover.uniqueid is None or - self._cover.uniqueid.count(':') != 7): - return None - serial = self._cover.uniqueid.split('-', 1)[0] - bridgeid = self.gateway.api.config.bridgeid - return { - 'connections': {(CONNECTION_ZIGBEE, serial)}, - 'identifiers': {(DECONZ_DOMAIN, serial)}, - 'manufacturer': self._cover.manufacturer, - 'model': self._cover.modelid, - 'name': self._cover.name, - 'sw_version': self._cover.swversion, - 'via_hub': (DECONZ_DOMAIN, bridgeid), - } + await self._device.async_set_state(data) class DeconzCoverZigbeeSpec(DeconzCover): @@ -178,12 +117,12 @@ class DeconzCoverZigbeeSpec(DeconzCover): @property def current_cover_position(self): """Return the current position of the cover.""" - return 100 - int(self._cover.brightness / 255 * 100) + return 100 - int(self._device.brightness / 255 * 100) @property def is_closed(self): """Return if the cover is closed.""" - return self._cover.state + return self._device.state async def async_set_cover_position(self, **kwargs): """Move the cover to a specific position.""" @@ -192,4 +131,4 @@ class DeconzCoverZigbeeSpec(DeconzCover): if position < 100: data['on'] = True data['bri'] = 255 - int(position / 100 * 255) - await self._cover.async_set_state(data) + await self._device.async_set_state(data) diff --git a/homeassistant/components/deconz/deconz_device.py b/homeassistant/components/deconz/deconz_device.py new file mode 100644 index 00000000000..bfcbd158b9f --- /dev/null +++ b/homeassistant/components/deconz/deconz_device.py @@ -0,0 +1,74 @@ +"""Base class for deCONZ devices.""" +from homeassistant.core import callback +from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import Entity + +from .const import DECONZ_REACHABLE, DOMAIN as DECONZ_DOMAIN + + +class DeconzDevice(Entity): + """Representation of a deCONZ device.""" + + def __init__(self, device, gateway): + """Set up device and add update callback to get data from websocket.""" + self._device = device + self.gateway = gateway + self.unsub_dispatcher = None + + async def async_added_to_hass(self): + """Subscribe to device events.""" + self._device.register_async_callback(self.async_update_callback) + self.gateway.deconz_ids[self.entity_id] = self._device.deconz_id + self.unsub_dispatcher = async_dispatcher_connect( + self.hass, DECONZ_REACHABLE, self.async_update_callback) + + async def async_will_remove_from_hass(self) -> None: + """Disconnect device object when removed.""" + if self.unsub_dispatcher is not None: + self.unsub_dispatcher() + self._device.remove_callback(self.async_update_callback) + self._device = None + + @callback + def async_update_callback(self, reason): + """Update the device's state.""" + self.async_schedule_update_ha_state() + + @property + def name(self): + """Return the name of the device.""" + return self._device.name + + @property + def unique_id(self): + """Return a unique identifier for this device.""" + return self._device.uniqueid + + @property + def available(self): + """Return True if device is available.""" + return self.gateway.available and self._device.reachable + + @property + def should_poll(self): + """No polling needed.""" + return False + + @property + def device_info(self): + """Return a device description for device registry.""" + if (self._device.uniqueid is None or + self._device.uniqueid.count(':') != 7): + return None + serial = self._device.uniqueid.split('-', 1)[0] + bridgeid = self.gateway.api.config.bridgeid + return { + 'connections': {(CONNECTION_ZIGBEE, serial)}, + 'identifiers': {(DECONZ_DOMAIN, serial)}, + 'manufacturer': self._device.manufacturer, + 'model': self._device.modelid, + 'name': self._device.name, + 'sw_version': self._device.swversion, + 'via_hub': (DECONZ_DOMAIN, bridgeid), + } diff --git a/homeassistant/components/deconz/light.py b/homeassistant/components/deconz/light.py index a7ad38dd8b7..f7c777b8100 100644 --- a/homeassistant/components/deconz/light.py +++ b/homeassistant/components/deconz/light.py @@ -10,13 +10,13 @@ from homeassistant.components.light import ( SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, SUPPORT_FLASH, SUPPORT_TRANSITION, Light) from homeassistant.core import callback -from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE from homeassistant.helpers.dispatcher import async_dispatcher_connect import homeassistant.util.color as color_util from .const import ( - CONF_ALLOW_DECONZ_GROUPS, DECONZ_REACHABLE, DOMAIN as DECONZ_DOMAIN, - COVER_TYPES, SWITCH_TYPES) + CONF_ALLOW_DECONZ_GROUPS, DOMAIN as DECONZ_DOMAIN, COVER_TYPES, + SWITCH_TYPES) +from .deconz_device import DeconzDevice DEPENDENCIES = ['deconz'] @@ -60,51 +60,30 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_group(gateway.api.groups.values()) -class DeconzLight(Light): +class DeconzLight(DeconzDevice, Light): """Representation of a deCONZ light.""" - def __init__(self, light, gateway): + def __init__(self, device, gateway): """Set up light and add update callback to get data from websocket.""" - self._light = light - self.gateway = gateway - self.unsub_dispatcher = None + super().__init__(device, gateway) self._features = SUPPORT_BRIGHTNESS self._features |= SUPPORT_FLASH self._features |= SUPPORT_TRANSITION - if self._light.ct is not None: + if self._device.ct is not None: self._features |= SUPPORT_COLOR_TEMP - if self._light.xy is not None: + if self._device.xy is not None: self._features |= SUPPORT_COLOR - if self._light.effect is not None: + if self._device.effect is not None: self._features |= SUPPORT_EFFECT - async def async_added_to_hass(self): - """Subscribe to lights events.""" - self._light.register_async_callback(self.async_update_callback) - self.gateway.deconz_ids[self.entity_id] = self._light.deconz_id - self.unsub_dispatcher = async_dispatcher_connect( - self.hass, DECONZ_REACHABLE, self.async_update_callback) - - async def async_will_remove_from_hass(self) -> None: - """Disconnect light object when removed.""" - if self.unsub_dispatcher is not None: - self.unsub_dispatcher() - self._light.remove_callback(self.async_update_callback) - self._light = None - - @callback - def async_update_callback(self, reason): - """Update the light's state.""" - self.async_schedule_update_ha_state() - @property def brightness(self): """Return the brightness of this light between 0..255.""" - return self._light.brightness + return self._device.brightness @property def effect_list(self): @@ -114,48 +93,28 @@ class DeconzLight(Light): @property def color_temp(self): """Return the CT color value.""" - if self._light.colormode != 'ct': + if self._device.colormode != 'ct': return None - return self._light.ct + return self._device.ct @property def hs_color(self): """Return the hs color value.""" - if self._light.colormode in ('xy', 'hs') and self._light.xy: - return color_util.color_xy_to_hs(*self._light.xy) + if self._device.colormode in ('xy', 'hs') and self._device.xy: + return color_util.color_xy_to_hs(*self._device.xy) return None @property def is_on(self): """Return true if light is on.""" - return self._light.state - - @property - def name(self): - """Return the name of the light.""" - return self._light.name - - @property - def unique_id(self): - """Return a unique identifier for this light.""" - return self._light.uniqueid + return self._device.state @property def supported_features(self): """Flag supported features.""" return self._features - @property - def available(self): - """Return True if light is available.""" - return self.gateway.available and self._light.reachable - - @property - def should_poll(self): - """No polling needed.""" - return False - async def async_turn_on(self, **kwargs): """Turn on light.""" data = {'on': True} @@ -186,7 +145,7 @@ class DeconzLight(Light): else: data['effect'] = 'none' - await self._light.async_set_state(data) + await self._device.async_set_state(data) async def async_turn_off(self, **kwargs): """Turn off light.""" @@ -204,31 +163,13 @@ class DeconzLight(Light): data['alert'] = 'lselect' del data['on'] - await self._light.async_set_state(data) + await self._device.async_set_state(data) @property def device_state_attributes(self): """Return the device state attributes.""" attributes = {} - attributes['is_deconz_group'] = self._light.type == 'LightGroup' - if self._light.type == 'LightGroup': - attributes['all_on'] = self._light.all_on + attributes['is_deconz_group'] = self._device.type == 'LightGroup' + if self._device.type == 'LightGroup': + attributes['all_on'] = self._device.all_on return attributes - - @property - def device_info(self): - """Return a device description for device registry.""" - if (self._light.uniqueid is None or - self._light.uniqueid.count(':') != 7): - return None - serial = self._light.uniqueid.split('-', 1)[0] - bridgeid = self.gateway.api.config.bridgeid - return { - 'connections': {(CONNECTION_ZIGBEE, serial)}, - 'identifiers': {(DECONZ_DOMAIN, serial)}, - 'manufacturer': self._light.manufacturer, - 'model': self._light.modelid, - 'name': self._light.name, - 'sw_version': self._light.swversion, - 'via_hub': (DECONZ_DOMAIN, bridgeid), - } diff --git a/homeassistant/components/deconz/sensor.py b/homeassistant/components/deconz/sensor.py index da5dcc84935..1913e3d5087 100644 --- a/homeassistant/components/deconz/sensor.py +++ b/homeassistant/components/deconz/sensor.py @@ -7,14 +7,12 @@ https://home-assistant.io/components/sensor.deconz/ from homeassistant.const import ( ATTR_BATTERY_LEVEL, ATTR_VOLTAGE, DEVICE_CLASS_BATTERY) from homeassistant.core import callback -from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import Entity from homeassistant.util import slugify from .const import ( - ATTR_DARK, ATTR_ON, CONF_ALLOW_CLIP_SENSOR, DECONZ_REACHABLE, - DOMAIN as DECONZ_DOMAIN) + ATTR_DARK, ATTR_ON, CONF_ALLOW_CLIP_SENSOR, DOMAIN as DECONZ_DOMAIN) +from .deconz_device import DeconzDevice DEPENDENCIES = ['deconz'] @@ -55,28 +53,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_sensor(gateway.api.sensors.values()) -class DeconzSensor(Entity): - """Representation of a sensor.""" - - def __init__(self, sensor, gateway): - """Set up sensor and add update callback to get data from websocket.""" - self._sensor = sensor - self.gateway = gateway - self.unsub_dispatcher = None - - async def async_added_to_hass(self): - """Subscribe to sensors events.""" - self._sensor.register_async_callback(self.async_update_callback) - self.gateway.deconz_ids[self.entity_id] = self._sensor.deconz_id - self.unsub_dispatcher = async_dispatcher_connect( - self.hass, DECONZ_REACHABLE, self.async_update_callback) - - async def async_will_remove_from_hass(self) -> None: - """Disconnect sensor object when removed.""" - if self.unsub_dispatcher is not None: - self.unsub_dispatcher() - self._sensor.remove_callback(self.async_update_callback) - self._sensor = None +class DeconzSensor(DeconzDevice): + """Representation of a deCONZ sensor.""" @callback def async_update_callback(self, reason): @@ -94,106 +72,52 @@ class DeconzSensor(Entity): @property def state(self): """Return the state of the sensor.""" - return self._sensor.state - - @property - def name(self): - """Return the name of the sensor.""" - return self._sensor.name - - @property - def unique_id(self): - """Return a unique identifier for this sensor.""" - return self._sensor.uniqueid + return self._device.state @property def device_class(self): """Return the class of the sensor.""" - return self._sensor.sensor_class + return self._device.sensor_class @property def icon(self): """Return the icon to use in the frontend.""" - return self._sensor.sensor_icon + return self._device.sensor_icon @property def unit_of_measurement(self): """Return the unit of measurement of this sensor.""" - return self._sensor.sensor_unit - - @property - def available(self): - """Return true if sensor is available.""" - return self.gateway.available and self._sensor.reachable - - @property - def should_poll(self): - """No polling needed.""" - return False + return self._device.sensor_unit @property def device_state_attributes(self): """Return the state attributes of the sensor.""" from pydeconz.sensor import LIGHTLEVEL attr = {} - if self._sensor.battery: - attr[ATTR_BATTERY_LEVEL] = self._sensor.battery - if self._sensor.on is not None: - attr[ATTR_ON] = self._sensor.on - if self._sensor.type in LIGHTLEVEL and self._sensor.dark is not None: - attr[ATTR_DARK] = self._sensor.dark + if self._device.battery: + attr[ATTR_BATTERY_LEVEL] = self._device.battery + if self._device.on is not None: + attr[ATTR_ON] = self._device.on + if self._device.type in LIGHTLEVEL and self._device.dark is not None: + attr[ATTR_DARK] = self._device.dark if self.unit_of_measurement == 'Watts': - attr[ATTR_CURRENT] = self._sensor.current - attr[ATTR_VOLTAGE] = self._sensor.voltage - if self._sensor.sensor_class == 'daylight': - attr[ATTR_DAYLIGHT] = self._sensor.daylight + attr[ATTR_CURRENT] = self._device.current + attr[ATTR_VOLTAGE] = self._device.voltage + if self._device.sensor_class == 'daylight': + attr[ATTR_DAYLIGHT] = self._device.daylight return attr - @property - def device_info(self): - """Return a device description for device registry.""" - if (self._sensor.uniqueid is None or - self._sensor.uniqueid.count(':') != 7): - return None - serial = self._sensor.uniqueid.split('-', 1)[0] - bridgeid = self.gateway.api.config.bridgeid - return { - 'connections': {(CONNECTION_ZIGBEE, serial)}, - 'identifiers': {(DECONZ_DOMAIN, serial)}, - 'manufacturer': self._sensor.manufacturer, - 'model': self._sensor.modelid, - 'name': self._sensor.name, - 'sw_version': self._sensor.swversion, - 'via_hub': (DECONZ_DOMAIN, bridgeid), - } - -class DeconzBattery(Entity): +class DeconzBattery(DeconzDevice): """Battery class for when a device is only represented as an event.""" - def __init__(self, sensor, gateway): + def __init__(self, device, gateway): """Register dispatcher callback for update of battery state.""" - self._sensor = sensor - self.gateway = gateway - self.unsub_dispatcher = None + super().__init__(device, gateway) - self._name = '{} {}'.format(self._sensor.name, 'Battery Level') + self._name = '{} {}'.format(self._device.name, 'Battery Level') self._unit_of_measurement = "%" - async def async_added_to_hass(self): - """Subscribe to sensors events.""" - self._sensor.register_async_callback(self.async_update_callback) - self.gateway.deconz_ids[self.entity_id] = self._sensor.deconz_id - self.unsub_dispatcher = async_dispatcher_connect( - self.hass, DECONZ_REACHABLE, self.async_update_callback) - - async def async_will_remove_from_hass(self) -> None: - """Disconnect sensor object when removed.""" - if self.unsub_dispatcher is not None: - self.unsub_dispatcher() - self._sensor.remove_callback(self.async_update_callback) - self._sensor = None - @callback def async_update_callback(self, reason): """Update the battery's state, if needed.""" @@ -203,18 +127,13 @@ class DeconzBattery(Entity): @property def state(self): """Return the state of the battery.""" - return self._sensor.battery + return self._device.battery @property def name(self): """Return the name of the battery.""" return self._name - @property - def unique_id(self): - """Return a unique identifier for the device.""" - return self._sensor.uniqueid - @property def device_class(self): """Return the class of the sensor.""" @@ -225,38 +144,10 @@ class DeconzBattery(Entity): """Return the unit of measurement of this entity.""" return self._unit_of_measurement - @property - def available(self): - """Return true if sensor is available.""" - return self.gateway.available and self._sensor.reachable - - @property - def should_poll(self): - """No polling needed.""" - return False - @property def device_state_attributes(self): """Return the state attributes of the battery.""" attr = { - ATTR_EVENT_ID: slugify(self._sensor.name), + ATTR_EVENT_ID: slugify(self._device.name), } return attr - - @property - def device_info(self): - """Return a device description for device registry.""" - if (self._sensor.uniqueid is None or - self._sensor.uniqueid.count(':') != 7): - return None - serial = self._sensor.uniqueid.split('-', 1)[0] - bridgeid = self.gateway.api.config.bridgeid - return { - 'connections': {(CONNECTION_ZIGBEE, serial)}, - 'identifiers': {(DECONZ_DOMAIN, serial)}, - 'manufacturer': self._sensor.manufacturer, - 'model': self._sensor.modelid, - 'name': self._sensor.name, - 'sw_version': self._sensor.swversion, - 'via_hub': (DECONZ_DOMAIN, bridgeid), - } diff --git a/homeassistant/components/deconz/switch.py b/homeassistant/components/deconz/switch.py index 6c64fce5bd5..64d93389670 100644 --- a/homeassistant/components/deconz/switch.py +++ b/homeassistant/components/deconz/switch.py @@ -6,11 +6,11 @@ https://home-assistant.io/components/switch.deconz/ """ from homeassistant.components.switch import SwitchDevice from homeassistant.core import callback -from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE from homeassistant.helpers.dispatcher import async_dispatcher_connect -from .const import ( - DECONZ_REACHABLE, DOMAIN as DECONZ_DOMAIN, POWER_PLUGS, SIRENS) +from .const import DOMAIN as DECONZ_DOMAIN, POWER_PLUGS, SIRENS +from .deconz_device import DeconzDevice + DEPENDENCIES = ['deconz'] @@ -45,106 +45,39 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_switch(gateway.api.lights.values()) -class DeconzSwitch(SwitchDevice): - """Representation of a deCONZ switch.""" - - def __init__(self, switch, gateway): - """Set up switch and add update callback to get data from websocket.""" - self._switch = switch - self.gateway = gateway - self.unsub_dispatcher = None - - async def async_added_to_hass(self): - """Subscribe to switches events.""" - self._switch.register_async_callback(self.async_update_callback) - self.gateway.deconz_ids[self.entity_id] = self._switch.deconz_id - self.unsub_dispatcher = async_dispatcher_connect( - self.hass, DECONZ_REACHABLE, self.async_update_callback) - - async def async_will_remove_from_hass(self) -> None: - """Disconnect switch object when removed.""" - if self.unsub_dispatcher is not None: - self.unsub_dispatcher() - self._switch.remove_callback(self.async_update_callback) - self._switch = None - - @callback - def async_update_callback(self, reason): - """Update the switch's state.""" - self.async_schedule_update_ha_state() - - @property - def name(self): - """Return the name of the switch.""" - return self._switch.name - - @property - def unique_id(self): - """Return a unique identifier for this switch.""" - return self._switch.uniqueid - - @property - def available(self): - """Return True if light is available.""" - return self.gateway.available and self._switch.reachable - - @property - def should_poll(self): - """No polling needed.""" - return False - - @property - def device_info(self): - """Return a device description for device registry.""" - if (self._switch.uniqueid is None or - self._switch.uniqueid.count(':') != 7): - return None - serial = self._switch.uniqueid.split('-', 1)[0] - bridgeid = self.gateway.api.config.bridgeid - return { - 'connections': {(CONNECTION_ZIGBEE, serial)}, - 'identifiers': {(DECONZ_DOMAIN, serial)}, - 'manufacturer': self._switch.manufacturer, - 'model': self._switch.modelid, - 'name': self._switch.name, - 'sw_version': self._switch.swversion, - 'via_hub': (DECONZ_DOMAIN, bridgeid), - } - - -class DeconzPowerPlug(DeconzSwitch): - """Representation of power plugs from deCONZ.""" +class DeconzPowerPlug(DeconzDevice, SwitchDevice): + """Representation of a deCONZ power plug.""" @property def is_on(self): """Return true if switch is on.""" - return self._switch.state + return self._device.state async def async_turn_on(self, **kwargs): """Turn on switch.""" data = {'on': True} - await self._switch.async_set_state(data) + await self._device.async_set_state(data) async def async_turn_off(self, **kwargs): """Turn off switch.""" data = {'on': False} - await self._switch.async_set_state(data) + await self._device.async_set_state(data) -class DeconzSiren(DeconzSwitch): - """Representation of sirens from deCONZ.""" +class DeconzSiren(DeconzDevice, SwitchDevice): + """Representation of a deCONZ siren.""" @property def is_on(self): """Return true if switch is on.""" - return self._switch.alert == 'lselect' + return self._device.alert == 'lselect' async def async_turn_on(self, **kwargs): """Turn on switch.""" data = {'alert': 'lselect'} - await self._switch.async_set_state(data) + await self._device.async_set_state(data) async def async_turn_off(self, **kwargs): """Turn off switch.""" data = {'alert': 'none'} - await self._switch.async_set_state(data) + await self._device.async_set_state(data) From 78da6828f09ebb8d21bab03bb0e5ab62b9f9e851 Mon Sep 17 00:00:00 2001 From: ehendrix23 Date: Wed, 16 Jan 2019 00:35:29 -0700 Subject: [PATCH 192/235] Reconnect and device name fix for harmony platform (#20108) * Update requirements Updated requirements * Add attributes Add firmware and config version attributes * Small bump for aioharmony Small version bump increase for aioharmony * Fix requirements file For some reason aioharmony ended up in there as a duplicate. Fixed it. * Fix for send command with named device * Update requirements to get reconnect fix * Set aioharmony version to 0.1.4 Version 0.1.4 has additional small fixes. * Keep trying to connect on startup Keep trying to connect to the HUB on startup * Revert rebase changes Revert some changes that should have been reverted back as part of rebase. * PlatformNotReady if unable to connect on startup Will call PlatformNotReady if unable to connect to Hub on startup * Increase aioharmony requirement to 0.1.5 Increase aioharmony requirement to 0.1.5 * Register callbacks when entity added to HASS Register the callbacks only once the entity has been added to HASS instead of during setup of platform. * Removed debug log in __init__ Removed debug log in __init__ --- homeassistant/components/remote/harmony.py | 65 +++++++++++++--------- requirements_all.txt | 2 +- 2 files changed, 40 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/remote/harmony.py b/homeassistant/components/remote/harmony.py index 07fd5831dbb..a5e4f5a8528 100644 --- a/homeassistant/components/remote/harmony.py +++ b/homeassistant/components/remote/harmony.py @@ -22,7 +22,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.exceptions import PlatformNotReady from homeassistant.util import slugify -REQUIREMENTS = ['aioharmony==0.1.2'] +REQUIREMENTS = ['aioharmony==0.1.5'] _LOGGER = logging.getLogger(__name__) @@ -106,6 +106,9 @@ async def async_setup_platform(hass, config, async_add_entities, try: device = HarmonyRemote( name, address, port, activity, harmony_conf_file, delay_secs) + if not await device.connect(): + raise PlatformNotReady + DEVICES.append(device) async_add_entities([device]) register_services(hass) @@ -152,33 +155,36 @@ class HarmonyRemote(remote.RemoteDevice): def __init__(self, name, host, port, activity, out_path, delay_secs): """Initialize HarmonyRemote class.""" - from aioharmony.harmonyapi import ( - HarmonyAPI as HarmonyClient, ClientCallbackType - ) + from aioharmony.harmonyapi import HarmonyAPI as HarmonyClient - _LOGGER.debug("%s: Device init started", name) self._name = name self.host = host self.port = port self._state = None self._current_activity = None self._default_activity = activity - self._client = HarmonyClient( - ip_address=host, - callbacks=ClientCallbackType( - new_activity=self.new_activity, - config_updated=self.new_config, - connect=self.got_connected, - disconnect=self.got_disconnected - ) - ) + self._client = HarmonyClient(ip_address=host) self._config_path = out_path self._delay_secs = delay_secs self._available = False async def async_added_to_hass(self): """Complete the initialization.""" + from aioharmony.harmonyapi import ClientCallbackType + _LOGGER.debug("%s: Harmony Hub added", self._name) + # Register the callbacks + self._client.callbacks = ClientCallbackType( + new_activity=self.new_activity, + config_updated=self.new_config, + connect=self.got_connected, + disconnect=self.got_disconnected + ) + + # Store Harmony HUB config, this will also update our current + # activity + await self.new_config() + import aioharmony.exceptions as aioexc async def shutdown(_): @@ -191,15 +197,6 @@ class HarmonyRemote(remote.RemoteDevice): self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, shutdown) - _LOGGER.debug("%s: Connecting", self._name) - try: - await self._client.connect() - except aioexc.TimeOut: - _LOGGER.error("%s: Connection timed-out", self._name) - else: - # Set initial state - self.new_activity(self._client.current_activity) - @property def name(self): """Return the Harmony device's name.""" @@ -225,6 +222,22 @@ class HarmonyRemote(remote.RemoteDevice): """Return True if connected to Hub, otherwise False.""" return self._available + async def connect(self): + """Connect to the Harmony HUB.""" + import aioharmony.exceptions as aioexc + + _LOGGER.debug("%s: Connecting", self._name) + try: + if not await self._client.connect(): + _LOGGER.warning("%s: Unable to connect to HUB.", self._name) + await self._client.close() + return False + except aioexc.TimeOut: + _LOGGER.warning("%s: Connection timed-out", self._name) + return False + + return True + def new_activity(self, activity_info: tuple) -> None: """Call for updating the current activity.""" activity_id, activity_name = activity_info @@ -340,7 +353,7 @@ class HarmonyRemote(remote.RemoteDevice): for _ in range(num_repeats): for single_command in command: send_command = SendCommandDevice( - device=device, + device=device_id, command=single_command, delay=0 ) @@ -360,8 +373,8 @@ class HarmonyRemote(remote.RemoteDevice): "%s: %s", result.command.command, result.command.device, - result.command.code, - result.command.msg + result.code, + result.msg ) async def change_channel(self, channel): diff --git a/requirements_all.txt b/requirements_all.txt index 5331e95ceee..b380ae85228 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -105,7 +105,7 @@ aiofreepybox==0.0.6 aioftp==0.12.0 # homeassistant.components.remote.harmony -aioharmony==0.1.2 +aioharmony==0.1.5 # homeassistant.components.emulated_hue # homeassistant.components.http From 75fa9b2fba2a4c912356bf13000f7d70bddd9c64 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 16 Jan 2019 00:07:32 -0800 Subject: [PATCH 193/235] Fix TTS say config validation (#20145) --- homeassistant/components/tts/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/tts/__init__.py b/homeassistant/components/tts/__init__.py index d06d28e8621..063ba428d4a 100644 --- a/homeassistant/components/tts/__init__.py +++ b/homeassistant/components/tts/__init__.py @@ -72,7 +72,7 @@ PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend({ SCHEMA_SERVICE_SAY = vol.Schema({ vol.Required(ATTR_MESSAGE): cv.string, vol.Optional(ATTR_CACHE): cv.boolean, - vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, + vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids, vol.Optional(ATTR_LANGUAGE): cv.string, vol.Optional(ATTR_OPTIONS): dict, }) From fe93ea9bdfb2c935dc419521b8a8a13f920141c6 Mon Sep 17 00:00:00 2001 From: Tim Gerla Date: Wed, 16 Jan 2019 03:08:23 -0500 Subject: [PATCH 194/235] Use the correct Unicode degree symbol (#20058) The previous symbol used for degrees was U+00BA, the "Masculine Ordinal Indicator". This patch changes the symbol to U+00B0, "Degree Sign", to match the rest of the Home Assistant system. --- homeassistant/components/sensor/ambient_station.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/sensor/ambient_station.py b/homeassistant/components/sensor/ambient_station.py index 0dfbbb63244..bc44f83d764 100644 --- a/homeassistant/components/sensor/ambient_station.py +++ b/homeassistant/components/sensor/ambient_station.py @@ -32,19 +32,19 @@ UNIT_SYSTEM = {UNITS_US: 0, UNITS_SI: 1} SCAN_INTERVAL = timedelta(seconds=300) SENSOR_TYPES = { - 'winddir': ['Wind Dir', 'º'], + 'winddir': ['Wind Dir', '°'], 'windspeedmph': ['Wind Speed', 'mph'], 'windgustmph': ['Wind Gust', 'mph'], 'maxdailygust': ['Max Gust', 'mph'], - 'windgustdir': ['Gust Dir', 'º'], + 'windgustdir': ['Gust Dir', '°'], 'windspdmph_avg2m': ['Wind Avg 2m', 'mph'], 'winddir_avg2m': ['Wind Dir Avg 2m', 'mph'], 'windspdmph_avg10m': ['Wind Avg 10m', 'mph'], - 'winddir_avg10m': ['Wind Dir Avg 10m', 'º'], + 'winddir_avg10m': ['Wind Dir Avg 10m', '°'], 'humidity': ['Humidity', '%'], 'humidityin': ['Humidity In', '%'], - 'tempf': ['Temp', ['ºF', 'ºC']], - 'tempinf': ['Inside Temp', ['ºF', 'ºC']], + 'tempf': ['Temp', ['°F', '°C']], + 'tempinf': ['Inside Temp', ['°F', '°C']], 'battout': ['Battery', ''], 'hourlyrainin': ['Hourly Rain Rate', 'in/hr'], 'dailyrainin': ['Daily Rain', 'in'], @@ -60,8 +60,8 @@ SENSOR_TYPES = { 'solarradiation': ['Solar Rad', 'W/m^2'], 'co2': ['co2', 'ppm'], 'lastRain': ['Last Rain', ''], - 'dewPoint': ['Dew Point', ['ºF', 'ºC']], - 'feelsLike': ['Feels Like', ['ºF', 'ºC']], + 'dewPoint': ['Dew Point', ['°F', '°C']], + 'feelsLike': ['Feels Like', ['°F', '°C']], } _LOGGER = logging.getLogger(__name__) From bc30491dc0a54de3f9b3b7faddab883f7ce34098 Mon Sep 17 00:00:00 2001 From: Rohan Kapoor Date: Wed, 16 Jan 2019 01:15:13 -0800 Subject: [PATCH 195/235] Add support for connecting to multiple zoneminder instances (#19955) * Add support for connecting to multiple zoneminder instances * Lint * Lint * Clean up and better error handling * Fix config validation * Lint --- homeassistant/components/camera/zoneminder.py | 21 +++--- homeassistant/components/sensor/zoneminder.py | 22 +++--- homeassistant/components/switch/zoneminder.py | 17 +++-- .../components/zoneminder/__init__.py | 72 ++++++++++++------- 4 files changed, 78 insertions(+), 54 deletions(-) diff --git a/homeassistant/components/camera/zoneminder.py b/homeassistant/components/camera/zoneminder.py index 9bde4ac099b..8556fbf1e35 100644 --- a/homeassistant/components/camera/zoneminder.py +++ b/homeassistant/components/camera/zoneminder.py @@ -19,17 +19,18 @@ DEPENDENCIES = ['zoneminder'] def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the ZoneMinder cameras.""" filter_urllib3_logging() - zm_client = hass.data[ZONEMINDER_DOMAIN] - - monitors = zm_client.get_monitors() - if not monitors: - _LOGGER.warning("Could not fetch monitors from ZoneMinder") - return - cameras = [] - for monitor in monitors: - _LOGGER.info("Initializing camera %s", monitor.id) - cameras.append(ZoneMinderCamera(monitor, zm_client.verify_ssl)) + for zm_client in hass.data[ZONEMINDER_DOMAIN].values(): + monitors = zm_client.get_monitors() + if not monitors: + _LOGGER.warning( + "Could not fetch monitors from ZoneMinder host: %s" + ) + return + + for monitor in monitors: + _LOGGER.info("Initializing camera %s", monitor.id) + cameras.append(ZoneMinderCamera(monitor, zm_client.verify_ssl)) add_entities(cameras) diff --git a/homeassistant/components/sensor/zoneminder.py b/homeassistant/components/sensor/zoneminder.py index 16513dc58de..abecc99e278 100644 --- a/homeassistant/components/sensor/zoneminder.py +++ b/homeassistant/components/sensor/zoneminder.py @@ -42,19 +42,21 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the ZoneMinder sensor platform.""" include_archived = config.get(CONF_INCLUDE_ARCHIVED) - zm_client = hass.data[ZONEMINDER_DOMAIN] - monitors = zm_client.get_monitors() - if not monitors: - _LOGGER.warning('Could not fetch any monitors from ZoneMinder') - sensors = [] - for monitor in monitors: - sensors.append(ZMSensorMonitors(monitor)) + for zm_client in hass.data[ZONEMINDER_DOMAIN].values(): + monitors = zm_client.get_monitors() + if not monitors: + _LOGGER.warning('Could not fetch any monitors from ZoneMinder') - for sensor in config[CONF_MONITORED_CONDITIONS]: - sensors.append(ZMSensorEvents(monitor, include_archived, sensor)) + for monitor in monitors: + sensors.append(ZMSensorMonitors(monitor)) - sensors.append(ZMSensorRunState(zm_client)) + for sensor in config[CONF_MONITORED_CONDITIONS]: + sensors.append( + ZMSensorEvents(monitor, include_archived, sensor) + ) + + sensors.append(ZMSensorRunState(zm_client)) add_entities(sensors) diff --git a/homeassistant/components/switch/zoneminder.py b/homeassistant/components/switch/zoneminder.py index c28fe843b90..b039d2e3ce9 100644 --- a/homeassistant/components/switch/zoneminder.py +++ b/homeassistant/components/switch/zoneminder.py @@ -29,16 +29,15 @@ def setup_platform(hass, config, add_entities, discovery_info=None): on_state = MonitorState(config.get(CONF_COMMAND_ON)) off_state = MonitorState(config.get(CONF_COMMAND_OFF)) - zm_client = hass.data[ZONEMINDER_DOMAIN] - - monitors = zm_client.get_monitors() - if not monitors: - _LOGGER.warning('Could not fetch monitors from ZoneMinder') - return - switches = [] - for monitor in monitors: - switches.append(ZMSwitchMonitors(monitor, on_state, off_state)) + for zm_client in hass.data[ZONEMINDER_DOMAIN].values(): + monitors = zm_client.get_monitors() + if not monitors: + _LOGGER.warning('Could not fetch monitors from ZoneMinder') + return + + for monitor in monitors: + switches.append(ZMSwitchMonitors(monitor, on_state, off_state)) add_entities(switches) diff --git a/homeassistant/components/zoneminder/__init__.py b/homeassistant/components/zoneminder/__init__.py index 8d603d04f59..258841e20d0 100644 --- a/homeassistant/components/zoneminder/__init__.py +++ b/homeassistant/components/zoneminder/__init__.py @@ -8,10 +8,10 @@ import logging import voluptuous as vol +import homeassistant.helpers.config_validation as cv from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, CONF_PATH, CONF_SSL, CONF_USERNAME, - CONF_VERIFY_SSL, ATTR_NAME) -import homeassistant.helpers.config_validation as cv + CONF_VERIFY_SSL, ATTR_NAME, ATTR_ID) _LOGGER = logging.getLogger(__name__) @@ -26,20 +26,23 @@ DEFAULT_TIMEOUT = 10 DEFAULT_VERIFY_SSL = True DOMAIN = 'zoneminder' +HOST_CONFIG_SCHEMA = vol.Schema({ + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_PASSWORD): cv.string, + vol.Optional(CONF_PATH, default=DEFAULT_PATH): cv.string, + vol.Optional(CONF_PATH_ZMS, default=DEFAULT_PATH_ZMS): cv.string, + vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean, + vol.Optional(CONF_USERNAME): cv.string, + vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean, +}) + CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_PASSWORD): cv.string, - vol.Optional(CONF_PATH, default=DEFAULT_PATH): cv.string, - vol.Optional(CONF_PATH_ZMS, default=DEFAULT_PATH_ZMS): cv.string, - vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean, - vol.Optional(CONF_USERNAME): cv.string, - vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean, - }) + DOMAIN: vol.All(cv.ensure_list, [HOST_CONFIG_SCHEMA]) }, extra=vol.ALLOW_EXTRA) SERVICE_SET_RUN_STATE = 'set_run_state' SET_RUN_STATE_SCHEMA = vol.Schema({ + vol.Required(ATTR_ID): cv.string, vol.Required(ATTR_NAME): cv.string }) @@ -48,27 +51,46 @@ def setup(hass, config): """Set up the ZoneMinder component.""" from zoneminder.zm import ZoneMinder - conf = config[DOMAIN] - if conf[CONF_SSL]: - schema = 'https' - else: - schema = 'http' + hass.data[DOMAIN] = {} - server_origin = '{}://{}'.format(schema, conf[CONF_HOST]) - hass.data[DOMAIN] = ZoneMinder(server_origin, - conf.get(CONF_USERNAME), - conf.get(CONF_PASSWORD), - conf.get(CONF_PATH), - conf.get(CONF_PATH_ZMS), - conf.get(CONF_VERIFY_SSL)) + success = True + + for conf in config[DOMAIN]: + if conf[CONF_SSL]: + schema = 'https' + else: + schema = 'http' + + host_name = conf[CONF_HOST] + server_origin = '{}://{}'.format(schema, host_name) + zm_client = ZoneMinder( + server_origin, + conf.get(CONF_USERNAME), + conf.get(CONF_PASSWORD), + conf.get(CONF_PATH), + conf.get(CONF_PATH_ZMS), + conf.get(CONF_VERIFY_SSL) + ) + hass.data[DOMAIN][host_name] = zm_client + + success = zm_client.login() and success def set_active_state(call): """Set the ZoneMinder run state to the given state name.""" - return hass.data[DOMAIN].set_active_state(call.data[ATTR_NAME]) + zm_id = call.data[ATTR_ID] + state_name = call.data[ATTR_NAME] + if zm_id not in hass.data[DOMAIN]: + _LOGGER.error('Invalid ZoneMinder host provided: %s', zm_id) + if not hass.data[DOMAIN][zm_id].set_active_state(state_name): + _LOGGER.error( + 'Unable to change ZoneMinder state. Host: %s, state: %s', + zm_id, + state_name + ) hass.services.register( DOMAIN, SERVICE_SET_RUN_STATE, set_active_state, schema=SET_RUN_STATE_SCHEMA ) - return hass.data[DOMAIN].login() + return success From 11c78d5de81162da91af0a8eac115764b60e3fad Mon Sep 17 00:00:00 2001 From: Rohan Kapoor Date: Wed, 16 Jan 2019 01:33:25 -0800 Subject: [PATCH 196/235] Embed geofency platform into component (#20083) * Embded geofency platform into component * #20083: Change docstring for geofency device tracker platform --- .../{device_tracker/geofency.py => geofency/device_tracker.py} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename homeassistant/components/{device_tracker/geofency.py => geofency/device_tracker.py} (94%) diff --git a/homeassistant/components/device_tracker/geofency.py b/homeassistant/components/geofency/device_tracker.py similarity index 94% rename from homeassistant/components/device_tracker/geofency.py rename to homeassistant/components/geofency/device_tracker.py index cec494f322c..af11194c1d6 100644 --- a/homeassistant/components/device_tracker/geofency.py +++ b/homeassistant/components/geofency/device_tracker.py @@ -1,5 +1,5 @@ """ -Support for the Geofency platform. +Support for the Geofency device tracker platform. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/device_tracker.geofency/ From 9d112dc3f0ffa54dfd2e2d05a1362dd50ec519e7 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Wed, 16 Jan 2019 18:54:11 +0100 Subject: [PATCH 197/235] Remove .isort because we use the config from setup.cfg (#20158) --- .isort.cfg | 2 -- 1 file changed, 2 deletions(-) delete mode 100644 .isort.cfg diff --git a/.isort.cfg b/.isort.cfg deleted file mode 100644 index 79a65508287..00000000000 --- a/.isort.cfg +++ /dev/null @@ -1,2 +0,0 @@ -[settings] -multi_line_output=4 From c8efbb2cdcfd7bd972d6eac6b9daa11c6f802976 Mon Sep 17 00:00:00 2001 From: cvwillegen Date: Wed, 16 Jan 2019 18:56:53 +0100 Subject: [PATCH 198/235] Fix link to documentation link, select read-only (#20155) - Fixed link to documentation page at the top of the sensor - Select the IMAP4 Inbox read-only so that reading e-mail does not mark messages as Read --- homeassistant/components/sensor/imap_email_content.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/imap_email_content.py b/homeassistant/components/sensor/imap_email_content.py index ed535b69a1d..3fec05860f2 100644 --- a/homeassistant/components/sensor/imap_email_content.py +++ b/homeassistant/components/sensor/imap_email_content.py @@ -2,7 +2,7 @@ Email sensor support. For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.email/ +https://home-assistant.io/components/sensor.imap_email_content/ """ import logging import datetime @@ -97,7 +97,7 @@ class EmailReader: """Read the next email from the email server.""" import imaplib try: - self.connection.select() + self.connection.select('Inbox', readonly=True) if not self._unread_ids: search = "SINCE {0:%d-%b-%Y}".format(datetime.date.today()) From 48127cade08fbac981a9e876682cb95eb4f582e5 Mon Sep 17 00:00:00 2001 From: Rohan Kapoor Date: Wed, 16 Jan 2019 10:01:08 -0800 Subject: [PATCH 199/235] Embed mailgun platform into component (#20147) * Embed mailgun platform into component * #20147: Update .coveragerc * #20147 update requirements.txt --- .coveragerc | 2 +- .../components/{notify/mailgun.py => mailgun/notify.py} | 2 +- requirements_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) rename homeassistant/components/{notify/mailgun.py => mailgun/notify.py} (98%) diff --git a/.coveragerc b/.coveragerc index 80b6305881c..857b6b90a22 100644 --- a/.coveragerc +++ b/.coveragerc @@ -239,7 +239,7 @@ omit = homeassistant/components/lutron_caseta.py homeassistant/components/*/lutron_caseta.py - homeassistant/components/*/mailgun.py + homeassistant/components/mailgun/notify.py homeassistant/components/matrix.py homeassistant/components/*/matrix.py diff --git a/homeassistant/components/notify/mailgun.py b/homeassistant/components/mailgun/notify.py similarity index 98% rename from homeassistant/components/notify/mailgun.py rename to homeassistant/components/mailgun/notify.py index 56b0ab7e333..d4052678d36 100644 --- a/homeassistant/components/notify/mailgun.py +++ b/homeassistant/components/mailgun/notify.py @@ -1,5 +1,5 @@ """ -Support for the Mailgun mail service. +Support for the Mailgun mail notification service. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/notify.mailgun/ diff --git a/requirements_all.txt b/requirements_all.txt index b380ae85228..fc7cf9c0e12 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1087,7 +1087,7 @@ pylutron-caseta==0.5.0 # homeassistant.components.lutron pylutron==0.2.0 -# homeassistant.components.notify.mailgun +# homeassistant.components.mailgun.notify pymailgunner==1.4 # homeassistant.components.media_player.mediaroom From 075b575bdeae69752939bd87f67c5467f59341b3 Mon Sep 17 00:00:00 2001 From: Nikolay Vasilchuk Date: Wed, 16 Jan 2019 21:03:53 +0300 Subject: [PATCH 200/235] Support device_class for rest sensor (#20132) * Ready * Tests fixed --- homeassistant/components/sensor/rest.py | 18 ++++++++++++++---- tests/components/sensor/test_rest.py | 19 +++++++++++++------ 2 files changed, 27 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/sensor/rest.py b/homeassistant/components/sensor/rest.py index f2fbe6cd191..5b92753eb90 100644 --- a/homeassistant/components/sensor/rest.py +++ b/homeassistant/components/sensor/rest.py @@ -11,12 +11,13 @@ import voluptuous as vol import requests from requests.auth import HTTPBasicAuth, HTTPDigestAuth -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA, DEVICE_CLASSES_SCHEMA) from homeassistant.const import ( CONF_AUTHENTICATION, CONF_FORCE_UPDATE, CONF_HEADERS, CONF_NAME, CONF_METHOD, CONF_PASSWORD, CONF_PAYLOAD, CONF_RESOURCE, CONF_UNIT_OF_MEASUREMENT, CONF_USERNAME, - CONF_VALUE_TEMPLATE, CONF_VERIFY_SSL, + CONF_VALUE_TEMPLATE, CONF_VERIFY_SSL, CONF_DEVICE_CLASS, HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION, STATE_UNKNOWN) from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.entity import Entity @@ -43,6 +44,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_PASSWORD): cv.string, vol.Optional(CONF_PAYLOAD): cv.string, vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, + vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, vol.Optional(CONF_USERNAME): cv.string, vol.Optional(CONF_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean, @@ -61,6 +63,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): password = config.get(CONF_PASSWORD) headers = config.get(CONF_HEADERS) unit = config.get(CONF_UNIT_OF_MEASUREMENT) + device_class = config.get(CONF_DEVICE_CLASS) value_template = config.get(CONF_VALUE_TEMPLATE) json_attrs = config.get(CONF_JSON_ATTRS) force_update = config.get(CONF_FORCE_UPDATE) @@ -83,7 +86,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None): # Must update the sensor now (including fetching the rest resource) to # ensure it's updating its state. add_entities([RestSensor( - hass, rest, name, unit, value_template, json_attrs, force_update + hass, rest, name, unit, device_class, + value_template, json_attrs, force_update )], True) @@ -91,13 +95,14 @@ class RestSensor(Entity): """Implementation of a REST sensor.""" def __init__(self, hass, rest, name, unit_of_measurement, - value_template, json_attrs, force_update): + device_class, value_template, json_attrs, force_update): """Initialize the REST sensor.""" self._hass = hass self.rest = rest self._name = name self._state = STATE_UNKNOWN self._unit_of_measurement = unit_of_measurement + self._device_class = device_class self._value_template = value_template self._json_attrs = json_attrs self._attributes = None @@ -113,6 +118,11 @@ class RestSensor(Entity): """Return the unit the value is expressed in.""" return self._unit_of_measurement + @property + def device_class(self): + """Return the class of this sensor.""" + return self._device_class + @property def available(self): """Return if the sensor data are available.""" diff --git a/tests/components/sensor/test_rest.py b/tests/components/sensor/test_rest.py index 2ce72fc4fc4..05ab628b1a8 100644 --- a/tests/components/sensor/test_rest.py +++ b/tests/components/sensor/test_rest.py @@ -135,13 +135,14 @@ class TestRestSensor(unittest.TestCase): '{ "key": "' + self.initial_state + '" }')) self.name = 'foo' self.unit_of_measurement = 'MB' + self.device_class = None self.value_template = template('{{ value_json.key }}') self.value_template.hass = self.hass self.force_update = False self.sensor = rest.RestSensor( self.hass, self.rest, self.name, self.unit_of_measurement, - self.value_template, [], self.force_update + self.device_class, self.value_template, [], self.force_update ) def tearDown(self): @@ -192,7 +193,8 @@ class TestRestSensor(unittest.TestCase): side_effect=self.update_side_effect( 'plain_state')) self.sensor = rest.RestSensor(self.hass, self.rest, self.name, - self.unit_of_measurement, None, [], + self.unit_of_measurement, + self.device_class, None, [], self.force_update) self.sensor.update() assert 'plain_state' == self.sensor.state @@ -204,7 +206,8 @@ class TestRestSensor(unittest.TestCase): side_effect=self.update_side_effect( '{ "key": "some_json_value" }')) self.sensor = rest.RestSensor(self.hass, self.rest, self.name, - self.unit_of_measurement, None, ['key'], + self.unit_of_measurement, + self.device_class, None, ['key'], self.force_update) self.sensor.update() assert 'some_json_value' == \ @@ -216,7 +219,8 @@ class TestRestSensor(unittest.TestCase): self.rest.update = Mock('rest.RestData.update', side_effect=self.update_side_effect(None)) self.sensor = rest.RestSensor(self.hass, self.rest, self.name, - self.unit_of_measurement, None, ['key'], + self.unit_of_measurement, + self.device_class, None, ['key'], self.force_update) self.sensor.update() assert {} == self.sensor.device_state_attributes @@ -229,7 +233,8 @@ class TestRestSensor(unittest.TestCase): side_effect=self.update_side_effect( '["list", "of", "things"]')) self.sensor = rest.RestSensor(self.hass, self.rest, self.name, - self.unit_of_measurement, None, ['key'], + self.unit_of_measurement, + self.device_class, None, ['key'], self.force_update) self.sensor.update() assert {} == self.sensor.device_state_attributes @@ -242,7 +247,8 @@ class TestRestSensor(unittest.TestCase): side_effect=self.update_side_effect( 'This is text rather than JSON data.')) self.sensor = rest.RestSensor(self.hass, self.rest, self.name, - self.unit_of_measurement, None, ['key'], + self.unit_of_measurement, + self.device_class, None, ['key'], self.force_update) self.sensor.update() assert {} == self.sensor.device_state_attributes @@ -256,6 +262,7 @@ class TestRestSensor(unittest.TestCase): '{ "key": "json_state_updated_value" }')) self.sensor = rest.RestSensor(self.hass, self.rest, self.name, self.unit_of_measurement, + self.device_class, self.value_template, ['key'], self.force_update) self.sensor.update() From b5bfc759ec99a60c17faa4a1a9375026e107961e Mon Sep 17 00:00:00 2001 From: Rohan Kapoor Date: Wed, 16 Jan 2019 10:56:25 -0800 Subject: [PATCH 201/235] Migrate gpslogger to the automatically generated webhook (#20079) * Migrate gpslogger to the automatically generated webhook * Lint * Lint and return error code --- .../gpslogger/.translations/en.json | 18 ++ .../components/gpslogger/__init__.py | 172 +++++++++--------- .../components/gpslogger/strings.json | 18 ++ homeassistant/config_entries.py | 1 + tests/components/gpslogger/test_init.py | 101 ++++------ 5 files changed, 165 insertions(+), 145 deletions(-) create mode 100644 homeassistant/components/gpslogger/.translations/en.json create mode 100644 homeassistant/components/gpslogger/strings.json diff --git a/homeassistant/components/gpslogger/.translations/en.json b/homeassistant/components/gpslogger/.translations/en.json new file mode 100644 index 00000000000..d5641ef5db8 --- /dev/null +++ b/homeassistant/components/gpslogger/.translations/en.json @@ -0,0 +1,18 @@ +{ + "config": { + "title": "GPSLogger Webhook", + "step": { + "user": { + "title": "Set up the GPSLogger Webhook", + "description": "Are you sure you want to set up the GPSLogger Webhook?" + } + }, + "abort": { + "one_instance_allowed": "Only a single instance is necessary.", + "not_internet_accessible": "Your Home Assistant instance needs to be accessible from the internet to receive messages from GPSLogger." + }, + "create_entry": { + "default": "To send events to Home Assistant, you will need to setup the webhook feature in GPSLogger.\n\nFill in the following info:\n\n- URL: `{webhook_url}`\n- Method: POST\n\nSee [the documentation]({docs_url}) for further details." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/gpslogger/__init__.py b/homeassistant/components/gpslogger/__init__.py index e978e3706be..4d1a5708331 100644 --- a/homeassistant/components/gpslogger/__init__.py +++ b/homeassistant/components/gpslogger/__init__.py @@ -5,110 +5,116 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/gpslogger/ """ import logging -from hmac import compare_digest import voluptuous as vol -from aiohttp.web_exceptions import HTTPUnauthorized -from aiohttp.web_request import Request +from aiohttp import web import homeassistant.helpers.config_validation as cv -from homeassistant.components.http import HomeAssistantView, CONF_API_PASSWORD -from homeassistant.const import CONF_PASSWORD, HTTP_UNPROCESSABLE_ENTITY +from homeassistant.components.device_tracker import ATTR_BATTERY +from homeassistant.components.device_tracker.tile import ATTR_ALTITUDE +from homeassistant.const import HTTP_UNPROCESSABLE_ENTITY, \ + HTTP_OK, ATTR_LATITUDE, ATTR_LONGITUDE, CONF_WEBHOOK_ID +from homeassistant.helpers import config_entry_flow from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.dispatcher import async_dispatcher_send _LOGGER = logging.getLogger(__name__) DOMAIN = 'gpslogger' -DEPENDENCIES = ['http'] - -CONFIG_SCHEMA = vol.Schema({ - vol.Optional(DOMAIN): vol.Schema({ - vol.Optional(CONF_PASSWORD): cv.string, - }), -}, extra=vol.ALLOW_EXTRA) - -URL = '/api/{}'.format(DOMAIN) +DEPENDENCIES = ['webhook'] TRACKER_UPDATE = '{}_tracker_update'.format(DOMAIN) +ATTR_ACCURACY = 'accuracy' +ATTR_ACTIVITY = 'activity' +ATTR_DEVICE = 'device' +ATTR_DIRECTION = 'direction' +ATTR_PROVIDER = 'provider' +ATTR_SPEED = 'speed' + +DEFAULT_ACCURACY = 200 +DEFAULT_BATTERY = -1 + + +def _id(value: str) -> str: + """Coerce id by removing '-'.""" + return value.replace('-', '') + + +WEBHOOK_SCHEMA = vol.Schema({ + vol.Required(ATTR_LATITUDE): cv.latitude, + vol.Required(ATTR_LONGITUDE): cv.longitude, + vol.Required(ATTR_DEVICE): _id, + vol.Optional(ATTR_ACCURACY, default=DEFAULT_ACCURACY): vol.Coerce(float), + vol.Optional(ATTR_BATTERY, default=DEFAULT_BATTERY): vol.Coerce(float), + vol.Optional(ATTR_SPEED): vol.Coerce(float), + vol.Optional(ATTR_DIRECTION): vol.Coerce(float), + vol.Optional(ATTR_ALTITUDE): vol.Coerce(float), + vol.Optional(ATTR_PROVIDER): cv.string, + vol.Optional(ATTR_ACTIVITY): cv.string +}) + async def async_setup(hass, hass_config): """Set up the GPSLogger component.""" - config = hass_config[DOMAIN] - hass.http.register_view(GPSLoggerView(config)) - hass.async_create_task( async_load_platform(hass, 'device_tracker', DOMAIN, {}, hass_config) ) return True -class GPSLoggerView(HomeAssistantView): - """View to handle GPSLogger requests.""" - - url = URL - name = 'api:gpslogger' - - def __init__(self, config): - """Initialize GPSLogger url endpoints.""" - self._password = config.get(CONF_PASSWORD) - # this component does not require external authentication if - # password is set - self.requires_auth = self._password is None - - async def get(self, request: Request): - """Handle for GPSLogger message received as GET.""" - hass = request.app['hass'] - data = request.query - - if self._password is not None: - authenticated = CONF_API_PASSWORD in data and compare_digest( - self._password, - data[CONF_API_PASSWORD] - ) - if not authenticated: - raise HTTPUnauthorized() - - if 'latitude' not in data or 'longitude' not in data: - return ('Latitude and longitude not specified.', - HTTP_UNPROCESSABLE_ENTITY) - - if 'device' not in data: - _LOGGER.error("Device id not specified") - return ('Device id not specified.', - HTTP_UNPROCESSABLE_ENTITY) - - device = data['device'].replace('-', '') - gps_location = (data['latitude'], data['longitude']) - accuracy = 200 - battery = -1 - - if 'accuracy' in data: - accuracy = int(float(data['accuracy'])) - if 'battery' in data: - battery = float(data['battery']) - - attrs = {} - if 'speed' in data: - attrs['speed'] = float(data['speed']) - if 'direction' in data: - attrs['direction'] = float(data['direction']) - if 'altitude' in data: - attrs['altitude'] = float(data['altitude']) - if 'provider' in data: - attrs['provider'] = data['provider'] - if 'activity' in data: - attrs['activity'] = data['activity'] - - async_dispatcher_send( - hass, - TRACKER_UPDATE, - device, - gps_location, - battery, - accuracy, - attrs +async def handle_webhook(hass, webhook_id, request): + """Handle incoming webhook with GPSLogger request.""" + try: + data = WEBHOOK_SCHEMA(dict(await request.post())) + except vol.MultipleInvalid as error: + return web.Response( + body=error.error_message, + status=HTTP_UNPROCESSABLE_ENTITY ) - return 'Setting location for {}'.format(device) + attrs = { + ATTR_SPEED: data.get(ATTR_SPEED), + ATTR_DIRECTION: data.get(ATTR_DIRECTION), + ATTR_ALTITUDE: data.get(ATTR_ALTITUDE), + ATTR_PROVIDER: data.get(ATTR_PROVIDER), + ATTR_ACTIVITY: data.get(ATTR_ACTIVITY) + } + + device = data[ATTR_DEVICE] + + async_dispatcher_send( + hass, + TRACKER_UPDATE, + device, + (data[ATTR_LATITUDE], data[ATTR_LONGITUDE]), + data[ATTR_BATTERY], + data[ATTR_ACCURACY], + attrs + ) + + return web.Response( + body='Setting location for {}'.format(device), + status=HTTP_OK + ) + + +async def async_setup_entry(hass, entry): + """Configure based on config entry.""" + hass.components.webhook.async_register( + DOMAIN, 'GPSLogger', entry.data[CONF_WEBHOOK_ID], handle_webhook) + return True + + +async def async_unload_entry(hass, entry): + """Unload a config entry.""" + hass.components.webhook.async_unregister(entry.data[CONF_WEBHOOK_ID]) + return True + +config_entry_flow.register_webhook_flow( + DOMAIN, + 'GPSLogger Webhook', + { + 'docs_url': 'https://www.home-assistant.io/components/gpslogger/' + } +) diff --git a/homeassistant/components/gpslogger/strings.json b/homeassistant/components/gpslogger/strings.json new file mode 100644 index 00000000000..d5641ef5db8 --- /dev/null +++ b/homeassistant/components/gpslogger/strings.json @@ -0,0 +1,18 @@ +{ + "config": { + "title": "GPSLogger Webhook", + "step": { + "user": { + "title": "Set up the GPSLogger Webhook", + "description": "Are you sure you want to set up the GPSLogger Webhook?" + } + }, + "abort": { + "one_instance_allowed": "Only a single instance is necessary.", + "not_internet_accessible": "Your Home Assistant instance needs to be accessible from the internet to receive messages from GPSLogger." + }, + "create_entry": { + "default": "To send events to Home Assistant, you will need to setup the webhook feature in GPSLogger.\n\nFill in the following info:\n\n- URL: `{webhook_url}`\n- Method: POST\n\nSee [the documentation]({docs_url}) for further details." + } + } +} \ No newline at end of file diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index dde542e5fc0..3d9196f26a6 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -142,6 +142,7 @@ FLOWS = [ 'esphome', 'emulated_roku', 'geofency', + 'gpslogger', 'hangouts', 'homematicip_cloud', 'hue', diff --git a/tests/components/gpslogger/test_init.py b/tests/components/gpslogger/test_init.py index 539b9d549d3..cf818e54911 100644 --- a/tests/components/gpslogger/test_init.py +++ b/tests/components/gpslogger/test_init.py @@ -1,29 +1,21 @@ """The tests the for GPSLogger device tracker platform.""" -from unittest.mock import patch +from unittest.mock import patch, Mock import pytest +from homeassistant import data_entry_flow from homeassistant.components import zone from homeassistant.components.device_tracker import \ DOMAIN as DEVICE_TRACKER_DOMAIN -from homeassistant.components.gpslogger import URL, DOMAIN -from homeassistant.components.http import CONF_API_PASSWORD +from homeassistant.components.gpslogger import DOMAIN from homeassistant.const import HTTP_OK, HTTP_UNPROCESSABLE_ENTITY, \ - STATE_HOME, STATE_NOT_HOME, HTTP_UNAUTHORIZED, CONF_PASSWORD + STATE_HOME, STATE_NOT_HOME from homeassistant.setup import async_setup_component HOME_LATITUDE = 37.239622 HOME_LONGITUDE = -115.815811 -def _url(data=None): - """Generate URL.""" - data = data or {} - data = "&".join(["{}={}".format(name, value) for - name, value in data.items()]) - return "{}?{}".format(URL, data) - - @pytest.fixture(autouse=True) def mock_dev_track(mock_device_tracker_conf): """Mock device tracker config loading.""" @@ -31,28 +23,14 @@ def mock_dev_track(mock_device_tracker_conf): @pytest.fixture -def authenticated_gpslogger_client(loop, hass, hass_client): - """Locative mock client (authenticated).""" - assert loop.run_until_complete(async_setup_component( - hass, DOMAIN, { - DOMAIN: {} - })) - - with patch('homeassistant.components.device_tracker.update_config'): - yield loop.run_until_complete(hass_client()) - - -@pytest.fixture -def unauthenticated_gpslogger_client(loop, hass, aiohttp_client): - """Locative mock client (unauthenticated).""" +def gpslogger_client(loop, hass, aiohttp_client): + """Mock client for GPSLogger (unauthenticated).""" assert loop.run_until_complete(async_setup_component( hass, 'persistent_notification', {})) assert loop.run_until_complete(async_setup_component( hass, DOMAIN, { - DOMAIN: { - CONF_PASSWORD: 'test' - } + DOMAIN: {} })) with patch('homeassistant.components.device_tracker.update_config'): @@ -72,31 +50,26 @@ def setup_zones(loop, hass): }})) -async def test_authentication(hass, unauthenticated_gpslogger_client): +@pytest.fixture +async def webhook_id(hass, gpslogger_client): + """Initialize the GPSLogger component and get the webhook_id.""" + hass.config.api = Mock(base_url='http://example.com') + result = await hass.config_entries.flow.async_init(DOMAIN, context={ + 'source': 'user' + }) + assert result['type'] == data_entry_flow.RESULT_TYPE_FORM, result + + result = await hass.config_entries.flow.async_configure( + result['flow_id'], {}) + assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + + return result['result'].data['webhook_id'] + + +async def test_missing_data(hass, gpslogger_client, webhook_id): """Test missing data.""" - data = { - 'latitude': 1.0, - 'longitude': 1.1, - 'device': '123', - CONF_API_PASSWORD: 'test' - } + url = '/api/webhook/{}'.format(webhook_id) - # No auth - req = await unauthenticated_gpslogger_client.get(_url({})) - await hass.async_block_till_done() - assert req.status == HTTP_UNAUTHORIZED - - # Authenticated - req = await unauthenticated_gpslogger_client.get(_url(data)) - await hass.async_block_till_done() - assert req.status == HTTP_OK - state_name = hass.states.get('{}.{}'.format(DEVICE_TRACKER_DOMAIN, - data['device'])).state - assert STATE_NOT_HOME == state_name - - -async def test_missing_data(hass, authenticated_gpslogger_client): - """Test missing data.""" data = { 'latitude': 1.0, 'longitude': 1.1, @@ -104,27 +77,29 @@ async def test_missing_data(hass, authenticated_gpslogger_client): } # No data - req = await authenticated_gpslogger_client.get(_url({})) + req = await gpslogger_client.post(url) await hass.async_block_till_done() assert req.status == HTTP_UNPROCESSABLE_ENTITY # No latitude copy = data.copy() del copy['latitude'] - req = await authenticated_gpslogger_client.get(_url(copy)) + req = await gpslogger_client.post(url, data=copy) await hass.async_block_till_done() assert req.status == HTTP_UNPROCESSABLE_ENTITY # No device copy = data.copy() del copy['device'] - req = await authenticated_gpslogger_client.get(_url(copy)) + req = await gpslogger_client.post(url, data=copy) await hass.async_block_till_done() assert req.status == HTTP_UNPROCESSABLE_ENTITY -async def test_enter_and_exit(hass, authenticated_gpslogger_client): +async def test_enter_and_exit(hass, gpslogger_client, webhook_id): """Test when there is a known zone.""" + url = '/api/webhook/{}'.format(webhook_id) + data = { 'latitude': HOME_LATITUDE, 'longitude': HOME_LONGITUDE, @@ -132,7 +107,7 @@ async def test_enter_and_exit(hass, authenticated_gpslogger_client): } # Enter the Home - req = await authenticated_gpslogger_client.get(_url(data)) + req = await gpslogger_client.post(url, data=data) await hass.async_block_till_done() assert req.status == HTTP_OK state_name = hass.states.get('{}.{}'.format(DEVICE_TRACKER_DOMAIN, @@ -140,7 +115,7 @@ async def test_enter_and_exit(hass, authenticated_gpslogger_client): assert STATE_HOME == state_name # Enter Home again - req = await authenticated_gpslogger_client.get(_url(data)) + req = await gpslogger_client.post(url, data=data) await hass.async_block_till_done() assert req.status == HTTP_OK state_name = hass.states.get('{}.{}'.format(DEVICE_TRACKER_DOMAIN, @@ -151,7 +126,7 @@ async def test_enter_and_exit(hass, authenticated_gpslogger_client): data['latitude'] = 0 # Enter Somewhere else - req = await authenticated_gpslogger_client.get(_url(data)) + req = await gpslogger_client.post(url, data=data) await hass.async_block_till_done() assert req.status == HTTP_OK state_name = hass.states.get('{}.{}'.format(DEVICE_TRACKER_DOMAIN, @@ -159,8 +134,10 @@ async def test_enter_and_exit(hass, authenticated_gpslogger_client): assert STATE_NOT_HOME == state_name -async def test_enter_with_attrs(hass, authenticated_gpslogger_client): +async def test_enter_with_attrs(hass, gpslogger_client, webhook_id): """Test when additional attributes are present.""" + url = '/api/webhook/{}'.format(webhook_id) + data = { 'latitude': 1.0, 'longitude': 1.1, @@ -174,13 +151,13 @@ async def test_enter_with_attrs(hass, authenticated_gpslogger_client): 'activity': 'running' } - req = await authenticated_gpslogger_client.get(_url(data)) + req = await gpslogger_client.post(url, data=data) await hass.async_block_till_done() assert req.status == HTTP_OK state = hass.states.get('{}.{}'.format(DEVICE_TRACKER_DOMAIN, data['device'])) assert STATE_NOT_HOME == state.state - assert 10 == state.attributes['gps_accuracy'] + assert 10.5 == state.attributes['gps_accuracy'] assert 10.0 == state.attributes['battery'] assert 100.0 == state.attributes['speed'] assert 105.32 == state.attributes['direction'] From 5e73846bcc40bc57e7c209776f30a5c3147b7d91 Mon Sep 17 00:00:00 2001 From: cvwillegen Date: Wed, 16 Jan 2019 19:59:54 +0100 Subject: [PATCH 202/235] imap_email_content: allow configuring folder to read. (#20160) * Update imap_email_content.py Add configuration for selection of IMAP folder to track * Update imap_email_content.py --- homeassistant/components/sensor/imap_email_content.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sensor/imap_email_content.py b/homeassistant/components/sensor/imap_email_content.py index 3fec05860f2..225ad08f7d1 100644 --- a/homeassistant/components/sensor/imap_email_content.py +++ b/homeassistant/components/sensor/imap_email_content.py @@ -22,6 +22,7 @@ _LOGGER = logging.getLogger(__name__) CONF_SERVER = 'server' CONF_SENDERS = 'senders' +CONF_FOLDER = 'folder' ATTR_FROM = 'from' ATTR_BODY = 'body' @@ -36,6 +37,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_SERVER): cv.string, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, vol.Optional(CONF_VALUE_TEMPLATE): cv.template, + vol.Optional(CONF_FOLDER, default='INBOX'): cv.string, }) @@ -43,7 +45,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Email sensor platform.""" reader = EmailReader( config.get(CONF_USERNAME), config.get(CONF_PASSWORD), - config.get(CONF_SERVER), config.get(CONF_PORT)) + config.get(CONF_SERVER), config.get(CONF_PORT), + config.get(CONF_FOLDER)) value_template = config.get(CONF_VALUE_TEMPLATE) if value_template is not None: @@ -61,12 +64,13 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class EmailReader: """A class to read emails from an IMAP server.""" - def __init__(self, user, password, server, port): + def __init__(self, user, password, server, port, folder): """Initialize the Email Reader.""" self._user = user self._password = password self._server = server self._port = port + self._folder = folder self._last_id = None self._unread_ids = deque([]) self.connection = None @@ -97,7 +101,7 @@ class EmailReader: """Read the next email from the email server.""" import imaplib try: - self.connection.select('Inbox', readonly=True) + self.connection.select(self._folder, readonly=True) if not self._unread_ids: search = "SINCE {0:%d-%b-%Y}".format(datetime.date.today()) From 8748ace24491b6a423574500372eff87e845e5fa Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Wed, 16 Jan 2019 14:06:22 -0500 Subject: [PATCH 203/235] Make imports relative in ZHA component (#20020) * make imports relative * remove cyclic import --- homeassistant/components/zha/__init__.py | 92 ++++++++++++++++++- homeassistant/components/zha/api.py | 2 +- homeassistant/components/zha/binary_sensor.py | 14 +-- homeassistant/components/zha/const.py | 82 ----------------- .../components/zha/entities/entity.py | 10 +- homeassistant/components/zha/fan.py | 8 +- homeassistant/components/zha/light.py | 16 ++-- homeassistant/components/zha/sensor.py | 10 +- homeassistant/components/zha/switch.py | 8 +- 9 files changed, 122 insertions(+), 120 deletions(-) diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index d47e42511f5..8ff4c80e0ea 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -12,8 +12,6 @@ import types import voluptuous as vol from homeassistant import config_entries, const as ha_const - -from homeassistant.components.zha.entities import ZhaDeviceEntity import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -25,13 +23,17 @@ from . import const as zha_const from .event import ZhaEvent, ZhaRelayEvent from . import api from .helpers import convert_ieee +from .entities import ZhaDeviceEntity from .const import ( COMPONENTS, CONF_BAUDRATE, CONF_DATABASE, CONF_DEVICE_CONFIG, CONF_RADIO_TYPE, CONF_USB_PATH, DATA_ZHA, DATA_ZHA_BRIDGE_ID, DATA_ZHA_CONFIG, DATA_ZHA_CORE_COMPONENT, DATA_ZHA_DISPATCHERS, DATA_ZHA_RADIO, DEFAULT_BAUDRATE, DEFAULT_DATABASE_NAME, DEFAULT_RADIO_TYPE, DOMAIN, ZHA_DISCOVERY_NEW, RadioType, - EVENTABLE_CLUSTERS, DATA_ZHA_CORE_EVENTS, ENABLE_QUIRKS) + EVENTABLE_CLUSTERS, DATA_ZHA_CORE_EVENTS, ENABLE_QUIRKS, + DEVICE_CLASS, SINGLE_INPUT_CLUSTER_DEVICE_CLASS, + SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS, CUSTOM_CLUSTER_MAPPINGS, + COMPONENT_CLUSTERS) REQUIREMENTS = [ 'bellows==0.7.0', @@ -93,6 +95,7 @@ async def async_setup_entry(hass, config_entry): Will automatically load components to support devices found on the network. """ + establish_device_mappings() hass.data[DATA_ZHA] = hass.data.get(DATA_ZHA, {}) hass.data[DATA_ZHA][DATA_ZHA_DISPATCHERS] = [] @@ -204,6 +207,88 @@ async def async_unload_entry(hass, config_entry): return True +def establish_device_mappings(): + """Establish mappings between ZCL objects and HA ZHA objects. + + These cannot be module level, as importing bellows must be done in a + in a function. + """ + from zigpy import zcl, quirks + from zigpy.profiles import PROFILES, zha, zll + from .sensor import RelativeHumiditySensor + + if zha.PROFILE_ID not in DEVICE_CLASS: + DEVICE_CLASS[zha.PROFILE_ID] = {} + if zll.PROFILE_ID not in DEVICE_CLASS: + DEVICE_CLASS[zll.PROFILE_ID] = {} + + EVENTABLE_CLUSTERS.append(zcl.clusters.general.AnalogInput.cluster_id) + EVENTABLE_CLUSTERS.append(zcl.clusters.general.LevelControl.cluster_id) + EVENTABLE_CLUSTERS.append(zcl.clusters.general.MultistateInput.cluster_id) + EVENTABLE_CLUSTERS.append(zcl.clusters.general.OnOff.cluster_id) + + DEVICE_CLASS[zha.PROFILE_ID].update({ + zha.DeviceType.ON_OFF_SWITCH: 'binary_sensor', + zha.DeviceType.LEVEL_CONTROL_SWITCH: 'binary_sensor', + zha.DeviceType.REMOTE_CONTROL: 'binary_sensor', + zha.DeviceType.SMART_PLUG: 'switch', + zha.DeviceType.LEVEL_CONTROLLABLE_OUTPUT: 'light', + zha.DeviceType.ON_OFF_LIGHT: 'light', + zha.DeviceType.DIMMABLE_LIGHT: 'light', + zha.DeviceType.COLOR_DIMMABLE_LIGHT: 'light', + zha.DeviceType.ON_OFF_LIGHT_SWITCH: 'binary_sensor', + zha.DeviceType.DIMMER_SWITCH: 'binary_sensor', + zha.DeviceType.COLOR_DIMMER_SWITCH: 'binary_sensor', + }) + DEVICE_CLASS[zll.PROFILE_ID].update({ + zll.DeviceType.ON_OFF_LIGHT: 'light', + zll.DeviceType.ON_OFF_PLUGIN_UNIT: 'switch', + zll.DeviceType.DIMMABLE_LIGHT: 'light', + zll.DeviceType.DIMMABLE_PLUGIN_UNIT: 'light', + zll.DeviceType.COLOR_LIGHT: 'light', + zll.DeviceType.EXTENDED_COLOR_LIGHT: 'light', + zll.DeviceType.COLOR_TEMPERATURE_LIGHT: 'light', + zll.DeviceType.COLOR_CONTROLLER: 'binary_sensor', + zll.DeviceType.COLOR_SCENE_CONTROLLER: 'binary_sensor', + zll.DeviceType.CONTROLLER: 'binary_sensor', + zll.DeviceType.SCENE_CONTROLLER: 'binary_sensor', + zll.DeviceType.ON_OFF_SENSOR: 'binary_sensor', + }) + + SINGLE_INPUT_CLUSTER_DEVICE_CLASS.update({ + zcl.clusters.general.OnOff: 'switch', + zcl.clusters.measurement.RelativeHumidity: 'sensor', + zcl.clusters.measurement.TemperatureMeasurement: 'sensor', + zcl.clusters.measurement.PressureMeasurement: 'sensor', + zcl.clusters.measurement.IlluminanceMeasurement: 'sensor', + zcl.clusters.smartenergy.Metering: 'sensor', + zcl.clusters.homeautomation.ElectricalMeasurement: 'sensor', + zcl.clusters.general.PowerConfiguration: 'sensor', + zcl.clusters.security.IasZone: 'binary_sensor', + zcl.clusters.measurement.OccupancySensing: 'binary_sensor', + zcl.clusters.hvac.Fan: 'fan', + }) + SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS.update({ + zcl.clusters.general.OnOff: 'binary_sensor', + }) + + # A map of device/cluster to component/sub-component + CUSTOM_CLUSTER_MAPPINGS.update({ + (quirks.smartthings.SmartthingsTemperatureHumiditySensor, 64581): + ('sensor', RelativeHumiditySensor) + }) + + # A map of hass components to all Zigbee clusters it could use + for profile_id, classes in DEVICE_CLASS.items(): + profile = PROFILES[profile_id] + for device_type, component in classes.items(): + if component not in COMPONENT_CLUSTERS: + COMPONENT_CLUSTERS[component] = (set(), set()) + clusters = profile.CLUSTERS[device_type] + COMPONENT_CLUSTERS[component][0].update(clusters[0]) + COMPONENT_CLUSTERS[component][1].update(clusters[1]) + + class ApplicationListener: """All handlers for events that happen on the ZigBee application.""" @@ -214,7 +299,6 @@ class ApplicationListener: self._component = EntityComponent(_LOGGER, DOMAIN, hass) self._device_registry = collections.defaultdict(list) self._events = {} - zha_const.populate_data() for component in COMPONENTS: hass.data[DATA_ZHA][component] = ( diff --git a/homeassistant/components/zha/api.py b/homeassistant/components/zha/api.py index a8aa7a2dbb9..308c221bf2f 100644 --- a/homeassistant/components/zha/api.py +++ b/homeassistant/components/zha/api.py @@ -9,9 +9,9 @@ import logging import voluptuous as vol from homeassistant.components import websocket_api -from homeassistant.components.zha.entities import ZhaDeviceEntity from homeassistant.const import ATTR_ENTITY_ID import homeassistant.helpers.config_validation as cv +from .entities import ZhaDeviceEntity from .const import ( DOMAIN, ATTR_CLUSTER_ID, ATTR_CLUSTER_TYPE, ATTR_ATTRIBUTE, ATTR_VALUE, ATTR_MANUFACTURER, ATTR_COMMAND, ATTR_COMMAND_TYPE, ATTR_ARGS, IN, OUT, diff --git a/homeassistant/components/zha/binary_sensor.py b/homeassistant/components/zha/binary_sensor.py index 0d426f0aa14..fce9376700e 100644 --- a/homeassistant/components/zha/binary_sensor.py +++ b/homeassistant/components/zha/binary_sensor.py @@ -7,16 +7,16 @@ at https://home-assistant.io/components/binary_sensor.zha/ import logging from homeassistant.components.binary_sensor import DOMAIN, BinarySensorDevice -from homeassistant.components.zha import helpers -from homeassistant.components.zha.const import ( - DATA_ZHA, DATA_ZHA_DISPATCHERS, REPORT_CONFIG_IMMEDIATE, ZHA_DISCOVERY_NEW) -from homeassistant.components.zha.entities import ZhaEntity from homeassistant.const import STATE_ON -from homeassistant.components.zha.entities.listeners import ( - OnOffListener, LevelListener -) from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.restore_state import RestoreEntity +from . import helpers +from .const import ( + DATA_ZHA, DATA_ZHA_DISPATCHERS, REPORT_CONFIG_IMMEDIATE, ZHA_DISCOVERY_NEW) +from .entities import ZhaEntity +from .entities.listeners import ( + OnOffListener, LevelListener +) _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/zha/const.py b/homeassistant/components/zha/const.py index ebcf020a9b8..91a06d666c0 100644 --- a/homeassistant/components/zha/const.py +++ b/homeassistant/components/zha/const.py @@ -92,85 +92,3 @@ REPORT_CONFIG_IMMEDIATE = (REPORT_CONFIG_MIN_INT_IMMEDIATE, REPORT_CONFIG_RPT_CHANGE) REPORT_CONFIG_OP = (REPORT_CONFIG_MIN_INT_OP, REPORT_CONFIG_MAX_INT, REPORT_CONFIG_RPT_CHANGE) - - -def populate_data(): - """Populate data using constants from bellows. - - These cannot be module level, as importing bellows must be done in a - in a function. - """ - from zigpy import zcl, quirks - from zigpy.profiles import PROFILES, zha, zll - from .sensor import RelativeHumiditySensor - - if zha.PROFILE_ID not in DEVICE_CLASS: - DEVICE_CLASS[zha.PROFILE_ID] = {} - if zll.PROFILE_ID not in DEVICE_CLASS: - DEVICE_CLASS[zll.PROFILE_ID] = {} - - EVENTABLE_CLUSTERS.append(zcl.clusters.general.AnalogInput.cluster_id) - EVENTABLE_CLUSTERS.append(zcl.clusters.general.LevelControl.cluster_id) - EVENTABLE_CLUSTERS.append(zcl.clusters.general.MultistateInput.cluster_id) - EVENTABLE_CLUSTERS.append(zcl.clusters.general.OnOff.cluster_id) - - DEVICE_CLASS[zha.PROFILE_ID].update({ - zha.DeviceType.ON_OFF_SWITCH: 'binary_sensor', - zha.DeviceType.LEVEL_CONTROL_SWITCH: 'binary_sensor', - zha.DeviceType.REMOTE_CONTROL: 'binary_sensor', - zha.DeviceType.SMART_PLUG: 'switch', - zha.DeviceType.LEVEL_CONTROLLABLE_OUTPUT: 'light', - zha.DeviceType.ON_OFF_LIGHT: 'light', - zha.DeviceType.DIMMABLE_LIGHT: 'light', - zha.DeviceType.COLOR_DIMMABLE_LIGHT: 'light', - zha.DeviceType.ON_OFF_LIGHT_SWITCH: 'binary_sensor', - zha.DeviceType.DIMMER_SWITCH: 'binary_sensor', - zha.DeviceType.COLOR_DIMMER_SWITCH: 'binary_sensor', - }) - DEVICE_CLASS[zll.PROFILE_ID].update({ - zll.DeviceType.ON_OFF_LIGHT: 'light', - zll.DeviceType.ON_OFF_PLUGIN_UNIT: 'switch', - zll.DeviceType.DIMMABLE_LIGHT: 'light', - zll.DeviceType.DIMMABLE_PLUGIN_UNIT: 'light', - zll.DeviceType.COLOR_LIGHT: 'light', - zll.DeviceType.EXTENDED_COLOR_LIGHT: 'light', - zll.DeviceType.COLOR_TEMPERATURE_LIGHT: 'light', - zll.DeviceType.COLOR_CONTROLLER: 'binary_sensor', - zll.DeviceType.COLOR_SCENE_CONTROLLER: 'binary_sensor', - zll.DeviceType.CONTROLLER: 'binary_sensor', - zll.DeviceType.SCENE_CONTROLLER: 'binary_sensor', - zll.DeviceType.ON_OFF_SENSOR: 'binary_sensor', - }) - - SINGLE_INPUT_CLUSTER_DEVICE_CLASS.update({ - zcl.clusters.general.OnOff: 'switch', - zcl.clusters.measurement.RelativeHumidity: 'sensor', - zcl.clusters.measurement.TemperatureMeasurement: 'sensor', - zcl.clusters.measurement.PressureMeasurement: 'sensor', - zcl.clusters.measurement.IlluminanceMeasurement: 'sensor', - zcl.clusters.smartenergy.Metering: 'sensor', - zcl.clusters.homeautomation.ElectricalMeasurement: 'sensor', - zcl.clusters.general.PowerConfiguration: 'sensor', - zcl.clusters.security.IasZone: 'binary_sensor', - zcl.clusters.measurement.OccupancySensing: 'binary_sensor', - zcl.clusters.hvac.Fan: 'fan', - }) - SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS.update({ - zcl.clusters.general.OnOff: 'binary_sensor', - }) - - # A map of device/cluster to component/sub-component - CUSTOM_CLUSTER_MAPPINGS.update({ - (quirks.smartthings.SmartthingsTemperatureHumiditySensor, 64581): - ('sensor', RelativeHumiditySensor) - }) - - # A map of hass components to all Zigbee clusters it could use - for profile_id, classes in DEVICE_CLASS.items(): - profile = PROFILES[profile_id] - for device_type, component in classes.items(): - if component not in COMPONENT_CLUSTERS: - COMPONENT_CLUSTERS[component] = (set(), set()) - clusters = profile.CLUSTERS[device_type] - COMPONENT_CLUSTERS[component][0].update(clusters[0]) - COMPONENT_CLUSTERS[component][1].update(clusters[1]) diff --git a/homeassistant/components/zha/entities/entity.py b/homeassistant/components/zha/entities/entity.py index 9247d6eeb00..8f8c8e58e05 100644 --- a/homeassistant/components/zha/entities/entity.py +++ b/homeassistant/components/zha/entities/entity.py @@ -8,16 +8,16 @@ import asyncio import logging from random import uniform -from homeassistant.components.zha.const import ( - DATA_ZHA, DATA_ZHA_BRIDGE_ID, DOMAIN, ATTR_CLUSTER_ID, ATTR_ATTRIBUTE, - ATTR_VALUE, ATTR_MANUFACTURER, ATTR_COMMAND, SERVER, ATTR_COMMAND_TYPE, - ATTR_ARGS, IN, OUT, CLIENT_COMMANDS, SERVER_COMMANDS) -from homeassistant.components.zha.helpers import bind_configure_reporting from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import callback from homeassistant.helpers import entity from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE from homeassistant.util import slugify +from ..const import ( + DATA_ZHA, DATA_ZHA_BRIDGE_ID, DOMAIN, ATTR_CLUSTER_ID, ATTR_ATTRIBUTE, + ATTR_VALUE, ATTR_MANUFACTURER, ATTR_COMMAND, SERVER, ATTR_COMMAND_TYPE, + ATTR_ARGS, IN, OUT, CLIENT_COMMANDS, SERVER_COMMANDS) +from ..helpers import bind_configure_reporting _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/zha/fan.py b/homeassistant/components/zha/fan.py index 1649f2e57ca..630ab3f7bb9 100644 --- a/homeassistant/components/zha/fan.py +++ b/homeassistant/components/zha/fan.py @@ -9,11 +9,11 @@ import logging from homeassistant.components.fan import ( DOMAIN, SPEED_HIGH, SPEED_LOW, SPEED_MEDIUM, SPEED_OFF, SUPPORT_SET_SPEED, FanEntity) -from homeassistant.components.zha import helpers -from homeassistant.components.zha.const import ( - DATA_ZHA, DATA_ZHA_DISPATCHERS, REPORT_CONFIG_OP, ZHA_DISCOVERY_NEW) -from homeassistant.components.zha.entities import ZhaEntity from homeassistant.helpers.dispatcher import async_dispatcher_connect +from . import helpers +from .const import ( + DATA_ZHA, DATA_ZHA_DISPATCHERS, REPORT_CONFIG_OP, ZHA_DISCOVERY_NEW) +from .entities import ZhaEntity DEPENDENCIES = ['zha'] diff --git a/homeassistant/components/zha/light.py b/homeassistant/components/zha/light.py index c1a875e465a..766608b35b1 100644 --- a/homeassistant/components/zha/light.py +++ b/homeassistant/components/zha/light.py @@ -7,16 +7,16 @@ at https://home-assistant.io/components/light.zha/ import logging from homeassistant.components import light -from homeassistant.components.zha import helpers -from homeassistant.components.zha.const import ( - DATA_ZHA, DATA_ZHA_DISPATCHERS, REPORT_CONFIG_ASAP, REPORT_CONFIG_DEFAULT, - REPORT_CONFIG_IMMEDIATE, ZHA_DISCOVERY_NEW) -from homeassistant.components.zha.entities import ZhaEntity -from homeassistant.components.zha.entities.listeners import ( - OnOffListener, LevelListener -) from homeassistant.helpers.dispatcher import async_dispatcher_connect import homeassistant.util.color as color_util +from . import helpers +from .const import ( + DATA_ZHA, DATA_ZHA_DISPATCHERS, REPORT_CONFIG_ASAP, REPORT_CONFIG_DEFAULT, + REPORT_CONFIG_IMMEDIATE, ZHA_DISCOVERY_NEW) +from .entities import ZhaEntity +from .entities.listeners import ( + OnOffListener, LevelListener +) _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index 41499cddcb2..dabbcb79815 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -7,14 +7,14 @@ at https://home-assistant.io/components/sensor.zha/ import logging from homeassistant.components.sensor import DOMAIN -from homeassistant.components.zha import helpers -from homeassistant.components.zha.const import ( - DATA_ZHA, DATA_ZHA_DISPATCHERS, REPORT_CONFIG_MAX_INT, - REPORT_CONFIG_MIN_INT, REPORT_CONFIG_RPT_CHANGE, ZHA_DISCOVERY_NEW) -from homeassistant.components.zha.entities import ZhaEntity from homeassistant.const import TEMP_CELSIUS from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.util.temperature import convert as convert_temperature +from . import helpers +from .const import ( + DATA_ZHA, DATA_ZHA_DISPATCHERS, REPORT_CONFIG_MAX_INT, + REPORT_CONFIG_MIN_INT, REPORT_CONFIG_RPT_CHANGE, ZHA_DISCOVERY_NEW) +from .entities import ZhaEntity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/zha/switch.py b/homeassistant/components/zha/switch.py index 0e1080664be..793da4e1e3a 100644 --- a/homeassistant/components/zha/switch.py +++ b/homeassistant/components/zha/switch.py @@ -7,11 +7,11 @@ at https://home-assistant.io/components/switch.zha/ import logging from homeassistant.components.switch import DOMAIN, SwitchDevice -from homeassistant.components.zha import helpers -from homeassistant.components.zha.const import ( - DATA_ZHA, DATA_ZHA_DISPATCHERS, REPORT_CONFIG_IMMEDIATE, ZHA_DISCOVERY_NEW) -from homeassistant.components.zha.entities import ZhaEntity from homeassistant.helpers.dispatcher import async_dispatcher_connect +from . import helpers +from .const import ( + DATA_ZHA, DATA_ZHA_DISPATCHERS, REPORT_CONFIG_IMMEDIATE, ZHA_DISCOVERY_NEW) +from .entities import ZhaEntity _LOGGER = logging.getLogger(__name__) From 368682647d186f516a5e5941267daef1156e461e Mon Sep 17 00:00:00 2001 From: emontnemery Date: Wed, 16 Jan 2019 22:50:21 +0100 Subject: [PATCH 204/235] Log exceptions thrown by MQTT message callbacks (#19977) * Log exceptions thrown by MQTT message callbacks * Fix tests * Correct method for skipping wrapper in traceback * Lint * Simplify traceback print * Add test * Move wrapper to common helper function * Typing * Lint --- homeassistant/components/mqtt/__init__.py | 7 +++- homeassistant/util/logging.py | 40 +++++++++++++++++++++- tests/components/mqtt/test_init.py | 17 +++++++++ tests/components/mqtt/test_subscription.py | 6 ++-- 4 files changed, 66 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index dbdb7f7eb32..fcaa05f7921 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -34,6 +34,7 @@ from homeassistant.loader import bind_hass from homeassistant.setup import async_prepare_setup_platform from homeassistant.util.async_ import ( run_callback_threadsafe, run_coroutine_threadsafe) +from homeassistant.util.logging import catch_log_exception # Loading the config flow file will register the flow from . import config_flow # noqa pylint: disable=unused-import @@ -311,7 +312,11 @@ async def async_subscribe(hass: HomeAssistantType, topic: str, Call the return value to unsubscribe. """ async_remove = await hass.data[DATA_MQTT].async_subscribe( - topic, msg_callback, qos, encoding) + topic, catch_log_exception( + msg_callback, lambda topic, msg, qos: + "Exception in {} when handling msg on '{}': '{}'".format( + msg_callback.__name__, topic, msg)), + qos, encoding) return async_remove diff --git a/homeassistant/util/logging.py b/homeassistant/util/logging.py index f2bf15d8a03..ae32566c73c 100644 --- a/homeassistant/util/logging.py +++ b/homeassistant/util/logging.py @@ -1,9 +1,12 @@ """Logging utilities.""" import asyncio from asyncio.events import AbstractEventLoop +from functools import wraps +import inspect import logging import threading -from typing import Optional +import traceback +from typing import Any, Callable, Optional from .async_ import run_coroutine_threadsafe @@ -121,3 +124,38 @@ class AsyncHandler: def name(self, name: str) -> None: """Wrap property get_name to handler.""" self.handler.set_name(name) # type: ignore + + +def catch_log_exception( + func: Callable[..., Any], + format_err: Callable[..., Any], + *args: Any) -> Callable[[], None]: + """Decorate an callback to catch and log exceptions.""" + def log_exception(*args: Any) -> None: + module_name = inspect.getmodule(inspect.trace()[1][0]).__name__ + # Do not print the wrapper in the traceback + frames = len(inspect.trace()) - 1 + exc_msg = traceback.format_exc(-frames) + friendly_msg = format_err(*args) + logging.getLogger(module_name).error('%s\n%s', friendly_msg, exc_msg) + + wrapper_func = None + if asyncio.iscoroutinefunction(func): + @wraps(func) + async def async_wrapper(*args: Any) -> None: + """Catch and log exception.""" + try: + await func(*args) + except Exception: # pylint: disable=broad-except + log_exception(*args) + wrapper_func = async_wrapper + else: + @wraps(func) + def wrapper(*args: Any) -> None: + """Catch and log exception.""" + try: + func(*args) + except Exception: # pylint: disable=broad-except + log_exception(*args) + wrapper_func = wrapper + return wrapper_func diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index 6652eddd20b..540cfe0369d 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -297,6 +297,23 @@ class TestMQTTCallbacks(unittest.TestCase): "b'\\x9a' on test-topic with encoding utf-8" in \ test_handle.output[0] + def test_message_callback_exception_gets_logged(self): + """Test exception raised by message handler.""" + @callback + def bad_handler(*args): + """Record calls.""" + raise Exception('This is a bad message callback') + mqtt.subscribe(self.hass, 'test-topic', bad_handler) + + with self.assertLogs(level='WARNING') as test_handle: + fire_mqtt_message(self.hass, 'test-topic', 'test') + + self.hass.block_till_done() + assert \ + "Exception in bad_handler when handling msg on 'test-topic':" \ + " 'test'" in \ + test_handle.output[0] + def test_all_subscriptions_run_when_decode_fails(self): """Test all other subscriptions still run when decode fails for one.""" mqtt.subscribe(self.hass, 'test-topic', self.record_calls, diff --git a/tests/components/mqtt/test_subscription.py b/tests/components/mqtt/test_subscription.py index 102b71d7b53..69386e2bad4 100644 --- a/tests/components/mqtt/test_subscription.py +++ b/tests/components/mqtt/test_subscription.py @@ -1,4 +1,6 @@ """The tests for the MQTT subscription component.""" +from unittest import mock + from homeassistant.core import callback from homeassistant.components.mqtt.subscription import ( async_subscribe_topics, async_unsubscribe_topics) @@ -135,7 +137,7 @@ async def test_qos_encoding_default(hass, mqtt_mock, caplog): {'test_topic1': {'topic': 'test-topic1', 'msg_callback': msg_callback}}) mock_mqtt.async_subscribe.assert_called_once_with( - 'test-topic1', msg_callback, 0, 'utf-8') + 'test-topic1', mock.ANY, 0, 'utf-8') async def test_qos_encoding_custom(hass, mqtt_mock, caplog): @@ -155,7 +157,7 @@ async def test_qos_encoding_custom(hass, mqtt_mock, caplog): 'qos': 1, 'encoding': 'utf-16'}}) mock_mqtt.async_subscribe.assert_called_once_with( - 'test-topic1', msg_callback, 1, 'utf-16') + 'test-topic1', mock.ANY, 1, 'utf-16') async def test_no_change(hass, mqtt_mock, caplog): From 9bb7e40ee38f7f7078bb6788b28ef2eab94efa94 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Wed, 16 Jan 2019 23:23:46 +0100 Subject: [PATCH 205/235] Upgrade aiohttp to 3.5.3 (#19957) * Upgrade aiohttp to 3.5.3 * Upgrade aiohttp to 3.5.4 * Remove test for webhook component from camera.push * Lint --- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- setup.py | 2 +- tests/components/camera/test_push.py | 17 +++-------------- 4 files changed, 6 insertions(+), 17 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index eec2f7dde8c..06577be4763 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -1,4 +1,4 @@ -aiohttp==3.5.1 +aiohttp==3.5.4 astral==1.7.1 async_timeout==3.0.1 attrs==18.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index fc7cf9c0e12..9595d8430e7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1,5 +1,5 @@ # Home Assistant core -aiohttp==3.5.1 +aiohttp==3.5.4 astral==1.7.1 async_timeout==3.0.1 attrs==18.2.0 diff --git a/setup.py b/setup.py index 6fa648af781..d8c2c57b3d3 100755 --- a/setup.py +++ b/setup.py @@ -32,7 +32,7 @@ PROJECT_URLS = { PACKAGES = find_packages(exclude=['tests', 'tests.*']) REQUIRES = [ - 'aiohttp==3.5.1', + 'aiohttp==3.5.4', 'astral==1.7.1', 'async_timeout==3.0.1', 'attrs==18.2.0', diff --git a/tests/components/camera/test_push.py b/tests/components/camera/test_push.py index be1d24ce34f..dc52965cbe0 100644 --- a/tests/components/camera/test_push.py +++ b/tests/components/camera/test_push.py @@ -4,14 +4,12 @@ import io from datetime import timedelta from homeassistant import core as ha -from homeassistant.components import webhook from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util -async def test_bad_posting(aioclient_mock, hass, aiohttp_client): +async def test_bad_posting(hass, aiohttp_client): """Test that posting to wrong api endpoint fails.""" - await async_setup_component(hass, webhook.DOMAIN, {}) await async_setup_component(hass, 'camera', { 'camera': { 'platform': 'push', @@ -23,17 +21,9 @@ async def test_bad_posting(aioclient_mock, hass, aiohttp_client): client = await aiohttp_client(hass.http.app) - # wrong webhook - files = {'image': io.BytesIO(b'fake')} - resp = await client.post('/api/webhood/camera.wrong', data=files) - assert resp.status == 404 - # missing file - camera_state = hass.states.get('camera.config_test') - assert camera_state.state == 'idle' - - resp = await client.post('/api/webhook/camera.config_test') - assert resp.status == 200 # webhooks always return 200 + async with client.post('/api/webhook/camera.config_test') as resp: + assert resp.status == 200 # webhooks always return 200 camera_state = hass.states.get('camera.config_test') assert camera_state.state == 'idle' # no file supplied we are still idle @@ -41,7 +31,6 @@ async def test_bad_posting(aioclient_mock, hass, aiohttp_client): async def test_posting_url(hass, aiohttp_client): """Test that posting to api endpoint works.""" - await async_setup_component(hass, webhook.DOMAIN, {}) await async_setup_component(hass, 'camera', { 'camera': { 'platform': 'push', From 84a2e5d8fbca4936932350a8669b0a69064e621e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 16 Jan 2019 15:03:05 -0800 Subject: [PATCH 206/235] Strip login username in backend (#20150) * Add modern mode to HA auth provider that strips usernames * Add tests for async_get_or_create_credentials * Fix test --- homeassistant/auth/providers/homeassistant.py | 47 +++- tests/auth/providers/test_homeassistant.py | 251 ++++++++++++++---- .../test_auth_provider_homeassistant.py | 8 +- 3 files changed, 245 insertions(+), 61 deletions(-) diff --git a/homeassistant/auth/providers/homeassistant.py b/homeassistant/auth/providers/homeassistant.py index 2c5a76d2c90..f5605886628 100644 --- a/homeassistant/auth/providers/homeassistant.py +++ b/homeassistant/auth/providers/homeassistant.py @@ -1,6 +1,7 @@ """Home Assistant auth provider.""" import base64 from collections import OrderedDict +import logging from typing import Any, Dict, List, Optional, cast import bcrypt @@ -51,6 +52,15 @@ class Data: self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY, private=True) self._data = None # type: Optional[Dict[str, Any]] + self.is_legacy = False + + @callback + def normalize_username(self, username: str) -> str: + """Normalize a username based on the mode.""" + if self.is_legacy: + return username + + return username.strip() async def async_load(self) -> None: """Load stored data.""" @@ -61,6 +71,20 @@ class Data: 'users': [] } + for user in data['users']: + username = user['username'] + + # check if we have unstripped usernames + if username != username.strip(): + self.is_legacy = True + + logging.getLogger(__name__).warning( + "Home Assistant auth provider is running in legacy mode " + "because we detected usernames that start or end in a " + "space. Please change the username.") + + break + self._data = data @property @@ -73,6 +97,7 @@ class Data: Raises InvalidAuth if auth invalid. """ + username = self.normalize_username(username) dummy = b'$2b$12$CiuFGszHx9eNHxPuQcwBWez4CwDTOcLTX5CbOpV6gef2nYuXkY7BO' found = None @@ -105,7 +130,10 @@ class Data: def add_auth(self, username: str, password: str) -> None: """Add a new authenticated user/pass.""" - if any(user['username'] == username for user in self.users): + username = self.normalize_username(username) + + if any(self.normalize_username(user['username']) == username + for user in self.users): raise InvalidUser self.users.append({ @@ -116,9 +144,11 @@ class Data: @callback def async_remove_auth(self, username: str) -> None: """Remove authentication.""" + username = self.normalize_username(username) + index = None for i, user in enumerate(self.users): - if user['username'] == username: + if self.normalize_username(user['username']) == username: index = i break @@ -132,8 +162,10 @@ class Data: Raises InvalidUser if user cannot be found. """ + username = self.normalize_username(username) + for user in self.users: - if user['username'] == username: + if self.normalize_username(user['username']) == username: user['password'] = self.hash_password( new_password, True).decode() break @@ -178,10 +210,15 @@ class HassAuthProvider(AuthProvider): async def async_get_or_create_credentials( self, flow_result: Dict[str, str]) -> Credentials: """Get credentials based on the flow result.""" - username = flow_result['username'] + if self.data is None: + await self.async_initialize() + assert self.data is not None + + norm_username = self.data.normalize_username + username = norm_username(flow_result['username']) for credential in await self.async_credentials(): - if credential.data['username'] == username: + if norm_username(credential.data['username']) == username: return credential # Create new credentials. diff --git a/tests/auth/providers/test_homeassistant.py b/tests/auth/providers/test_homeassistant.py index d3fa27b9f5b..b654b42fb35 100644 --- a/tests/auth/providers/test_homeassistant.py +++ b/tests/auth/providers/test_homeassistant.py @@ -1,5 +1,5 @@ """Test the Home Assistant local auth provider.""" -from unittest.mock import Mock +from unittest.mock import Mock, patch import pytest import voluptuous as vol @@ -9,6 +9,8 @@ from homeassistant.auth import auth_manager_from_config, auth_store from homeassistant.auth.providers import ( auth_provider_from_config, homeassistant as hass_auth) +from tests.common import mock_coro + @pytest.fixture def data(hass): @@ -18,17 +20,13 @@ def data(hass): return data -async def test_adding_user(data, hass): - """Test adding a user.""" - data.add_auth('test-user', 'test-pass') - data.validate_login('test-user', 'test-pass') - - -async def test_adding_user_duplicate_username(data, hass): - """Test adding a user with duplicate username.""" - data.add_auth('test-user', 'test-pass') - with pytest.raises(hass_auth.InvalidUser): - data.add_auth('test-user', 'other-pass') +@pytest.fixture +def legacy_data(hass): + """Create a loaded legacy data class.""" + data = hass_auth.Data(hass) + hass.loop.run_until_complete(data.async_load()) + data.is_legacy = True + return data async def test_validating_password_invalid_user(data, hass): @@ -37,30 +35,72 @@ async def test_validating_password_invalid_user(data, hass): data.validate_login('non-existing', 'pw') +async def test_not_allow_set_id(): + """Test we are not allowed to set an ID in config.""" + hass = Mock() + with pytest.raises(vol.Invalid): + await auth_provider_from_config(hass, None, { + 'type': 'homeassistant', + 'id': 'invalid', + }) + + +async def test_new_users_populate_values(hass, data): + """Test that we populate data for new users.""" + data.add_auth('hello', 'test-pass') + await data.async_save() + + manager = await auth_manager_from_config(hass, [{ + 'type': 'homeassistant' + }], []) + provider = manager.auth_providers[0] + credentials = await provider.async_get_or_create_credentials({ + 'username': 'hello' + }) + user = await manager.async_get_or_create_user(credentials) + assert user.name == 'hello' + assert user.is_active + + +async def test_changing_password_raises_invalid_user(data, hass): + """Test that changing password raises invalid user.""" + with pytest.raises(hass_auth.InvalidUser): + data.change_password('non-existing', 'pw') + + +# Modern mode + +async def test_adding_user(data, hass): + """Test adding a user.""" + data.add_auth('test-user', 'test-pass') + data.validate_login('test-user', 'test-pass') + data.validate_login(' test-user ', 'test-pass') + + +async def test_adding_user_duplicate_username(data, hass): + """Test adding a user with duplicate username.""" + data.add_auth('test-user', 'test-pass') + with pytest.raises(hass_auth.InvalidUser): + data.add_auth('test-user ', 'other-pass') + + async def test_validating_password_invalid_password(data, hass): """Test validating an invalid password.""" data.add_auth('test-user', 'test-pass') with pytest.raises(hass_auth.InvalidAuth): - data.validate_login('test-user', 'invalid-pass') + data.validate_login(' test-user ', 'invalid-pass') async def test_changing_password(data, hass): """Test adding a user.""" - user = 'test-user' - data.add_auth(user, 'test-pass') - data.change_password(user, 'new-pass') + data.add_auth('test-user', 'test-pass') + data.change_password('test-user ', 'new-pass') with pytest.raises(hass_auth.InvalidAuth): - data.validate_login(user, 'test-pass') + data.validate_login('test-user', 'test-pass') - data.validate_login(user, 'new-pass') - - -async def test_changing_password_raises_invalid_user(data, hass): - """Test that we initialize an empty config.""" - with pytest.raises(hass_auth.InvalidUser): - data.change_password('non-existing', 'pw') + data.validate_login('test-user', 'new-pass') async def test_login_flow_validates(data, hass): @@ -81,6 +121,110 @@ async def test_login_flow_validates(data, hass): 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 + assert result['data']['username'] == 'test-user' + + +async def test_saving_loading(data, hass): + """Test saving and loading JSON.""" + data.add_auth('test-user', 'test-pass') + data.add_auth('second-user', 'second-pass') + await data.async_save() + + data = hass_auth.Data(hass) + await data.async_load() + data.validate_login('test-user ', 'test-pass') + data.validate_login('second-user ', 'second-pass') + + +async def test_get_or_create_credentials(hass, data): + """Test that we can get or create credentials.""" + manager = await auth_manager_from_config(hass, [{ + 'type': 'homeassistant' + }], []) + provider = manager.auth_providers[0] + provider.data = data + credentials1 = await provider.async_get_or_create_credentials({ + 'username': 'hello' + }) + with patch.object(provider, 'async_credentials', + return_value=mock_coro([credentials1])): + credentials2 = await provider.async_get_or_create_credentials({ + 'username': 'hello ' + }) + assert credentials1 is credentials2 + + +# Legacy mode + +async def test_legacy_adding_user(legacy_data, hass): + """Test in legacy mode adding a user.""" + legacy_data.add_auth('test-user', 'test-pass') + legacy_data.validate_login('test-user', 'test-pass') + + +async def test_legacy_adding_user_duplicate_username(legacy_data, hass): + """Test in legacy mode adding a user with duplicate username.""" + legacy_data.add_auth('test-user', 'test-pass') + with pytest.raises(hass_auth.InvalidUser): + legacy_data.add_auth('test-user', 'other-pass') + + +async def test_legacy_validating_password_invalid_password(legacy_data, hass): + """Test in legacy mode validating an invalid password.""" + legacy_data.add_auth('test-user', 'test-pass') + + with pytest.raises(hass_auth.InvalidAuth): + legacy_data.validate_login('test-user', 'invalid-pass') + + +async def test_legacy_changing_password(legacy_data, hass): + """Test in legacy mode adding a user.""" + user = 'test-user' + legacy_data.add_auth(user, 'test-pass') + legacy_data.change_password(user, 'new-pass') + + with pytest.raises(hass_auth.InvalidAuth): + legacy_data.validate_login(user, 'test-pass') + + legacy_data.validate_login(user, 'new-pass') + + +async def test_legacy_changing_password_raises_invalid_user(legacy_data, hass): + """Test in legacy mode that we initialize an empty config.""" + with pytest.raises(hass_auth.InvalidUser): + legacy_data.change_password('non-existing', 'pw') + + +async def test_legacy_login_flow_validates(legacy_data, hass): + """Test in legacy mode login flow.""" + legacy_data.add_auth('test-user', 'test-pass') + await legacy_data.async_save() + + provider = hass_auth.HassAuthProvider(hass, auth_store.AuthStore(hass), + {'type': 'homeassistant'}) + flow = await provider.async_login_flow({}) + result = await flow.async_step_init() + assert result['type'] == data_entry_flow.RESULT_TYPE_FORM + + 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', @@ -96,40 +240,43 @@ async def test_login_flow_validates(data, hass): assert result['data']['username'] == 'test-user' -async def test_saving_loading(data, hass): - """Test saving and loading JSON.""" - data.add_auth('test-user', 'test-pass') - data.add_auth('second-user', 'second-pass') - await data.async_save() +async def test_legacy_saving_loading(legacy_data, hass): + """Test in legacy mode saving and loading JSON.""" + legacy_data.add_auth('test-user', 'test-pass') + legacy_data.add_auth('second-user', 'second-pass') + await legacy_data.async_save() - data = hass_auth.Data(hass) - await data.async_load() - data.validate_login('test-user', 'test-pass') - data.validate_login('second-user', 'second-pass') + legacy_data = hass_auth.Data(hass) + await legacy_data.async_load() + legacy_data.is_legacy = True + legacy_data.validate_login('test-user', 'test-pass') + legacy_data.validate_login('second-user', 'second-pass') + + with pytest.raises(hass_auth.InvalidAuth): + legacy_data.validate_login('test-user ', 'test-pass') -async def test_not_allow_set_id(): - """Test we are not allowed to set an ID in config.""" - hass = Mock() - with pytest.raises(vol.Invalid): - await auth_provider_from_config(hass, None, { - 'type': 'homeassistant', - 'id': 'invalid', - }) - - -async def test_new_users_populate_values(hass, data): - """Test that we populate data for new users.""" - data.add_auth('hello', 'test-pass') - await data.async_save() - +async def test_legacy_get_or_create_credentials(hass, legacy_data): + """Test in legacy mode that we can get or create credentials.""" manager = await auth_manager_from_config(hass, [{ 'type': 'homeassistant' }], []) provider = manager.auth_providers[0] - credentials = await provider.async_get_or_create_credentials({ + provider.data = legacy_data + credentials1 = await provider.async_get_or_create_credentials({ 'username': 'hello' }) - user = await manager.async_get_or_create_user(credentials) - assert user.name == 'hello' - assert user.is_active + + with patch.object(provider, 'async_credentials', + return_value=mock_coro([credentials1])): + credentials2 = await provider.async_get_or_create_credentials({ + 'username': 'hello' + }) + assert credentials1 is credentials2 + + with patch.object(provider, 'async_credentials', + return_value=mock_coro([credentials1])): + credentials3 = await provider.async_get_or_create_credentials({ + 'username': 'hello ' + }) + assert credentials1 is not credentials3 diff --git a/tests/components/config/test_auth_provider_homeassistant.py b/tests/components/config/test_auth_provider_homeassistant.py index a4c4c5a3c5a..4cbf3493a93 100644 --- a/tests/components/config/test_auth_provider_homeassistant.py +++ b/tests/components/config/test_auth_provider_homeassistant.py @@ -168,10 +168,6 @@ async def test_delete_removes_credential(hass, hass_ws_client, client = await hass_ws_client(hass, hass_access_token) user = MockUser().add_to_hass(hass) - user.credentials.append( - await hass.auth.auth_providers[0].async_get_or_create_credentials({ - 'username': 'test-user'})) - hass_storage[prov_ha.STORAGE_KEY] = { 'version': 1, 'data': { @@ -181,6 +177,10 @@ async def test_delete_removes_credential(hass, hass_ws_client, } } + user.credentials.append( + await hass.auth.auth_providers[0].async_get_or_create_credentials({ + 'username': 'test-user'})) + await client.send_json({ 'id': 5, 'type': auth_ha.WS_TYPE_DELETE, From 06440bf07616280063beadbe413d6a37b9c1eacf Mon Sep 17 00:00:00 2001 From: damarco Date: Thu, 17 Jan 2019 01:07:40 +0100 Subject: [PATCH 207/235] Bump pynuki to 1.3.2 (#20173) --- homeassistant/components/lock/nuki.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/lock/nuki.py b/homeassistant/components/lock/nuki.py index 689ec31fc7c..8af798e31e0 100644 --- a/homeassistant/components/lock/nuki.py +++ b/homeassistant/components/lock/nuki.py @@ -15,7 +15,7 @@ from homeassistant.const import ( import homeassistant.helpers.config_validation as cv from homeassistant.helpers.service import extract_entity_ids -REQUIREMENTS = ['pynuki==1.3.1'] +REQUIREMENTS = ['pynuki==1.3.2'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 9595d8430e7..b6d771900b9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1124,7 +1124,7 @@ pynetgear==0.5.2 pynetio==0.1.9.1 # homeassistant.components.lock.nuki -pynuki==1.3.1 +pynuki==1.3.2 # homeassistant.components.sensor.nut pynut2==2.1.2 From 85404783d64bc86f8b3632d0656ad4576b4ae519 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Thu, 17 Jan 2019 01:08:41 +0100 Subject: [PATCH 208/235] Add Xiaomi Airpurifier Pro V7 support (#20093) * Add Xiaomi Air Purifier Pro V7 support * Reorder the model list * Improve the list of supported attributes/properties * Fix lint --- homeassistant/components/fan/xiaomi_miio.py | 100 ++++++++++++++------ 1 file changed, 71 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/fan/xiaomi_miio.py b/homeassistant/components/fan/xiaomi_miio.py index e6349782cd1..d3b52622a95 100644 --- a/homeassistant/components/fan/xiaomi_miio.py +++ b/homeassistant/components/fan/xiaomi_miio.py @@ -26,10 +26,23 @@ DEFAULT_NAME = 'Xiaomi Miio Device' DATA_KEY = 'fan.xiaomi_miio' CONF_MODEL = 'model' -MODEL_AIRPURIFIER_PRO = 'zhimi.airpurifier.v6' +MODEL_AIRPURIFIER_V1 = 'zhimi.airpurifier.v1' +MODEL_AIRPURIFIER_V2 = 'zhimi.airpurifier.v2' MODEL_AIRPURIFIER_V3 = 'zhimi.airpurifier.v3' +MODEL_AIRPURIFIER_V5 = 'zhimi.airpurifier.v5' +MODEL_AIRPURIFIER_PRO = 'zhimi.airpurifier.v6' +MODEL_AIRPURIFIER_PRO_V7 = 'zhimi.airpurifier.v7' +MODEL_AIRPURIFIER_M1 = 'zhimi.airpurifier.m1' +MODEL_AIRPURIFIER_M2 = 'zhimi.airpurifier.m2' +MODEL_AIRPURIFIER_MA1 = 'zhimi.airpurifier.ma1' +MODEL_AIRPURIFIER_MA2 = 'zhimi.airpurifier.ma2' +MODEL_AIRPURIFIER_SA1 = 'zhimi.airpurifier.sa1' +MODEL_AIRPURIFIER_SA2 = 'zhimi.airpurifier.sa2' +MODEL_AIRPURIFIER_MC1 = 'zhimi.airpurifier.mc1' + MODEL_AIRHUMIDIFIER_V1 = 'zhimi.humidifier.v1' MODEL_AIRHUMIDIFIER_CA = 'zhimi.humidifier.ca1' + MODEL_AIRFRESH_VA2 = 'zhimi.airfresh.va2' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ @@ -37,21 +50,22 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_TOKEN): vol.All(cv.string, vol.Length(min=32, max=32)), vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_MODEL): vol.In( - ['zhimi.airpurifier.m1', - 'zhimi.airpurifier.m2', - 'zhimi.airpurifier.ma1', - 'zhimi.airpurifier.ma2', - 'zhimi.airpurifier.sa1', - 'zhimi.airpurifier.sa2', - 'zhimi.airpurifier.v1', - 'zhimi.airpurifier.v2', - 'zhimi.airpurifier.v3', - 'zhimi.airpurifier.v5', - 'zhimi.airpurifier.v6', - 'zhimi.airpurifier.mc1', - 'zhimi.humidifier.v1', - 'zhimi.humidifier.ca1', - 'zhimi.airfresh.va2', + [MODEL_AIRPURIFIER_V1, + MODEL_AIRPURIFIER_V2, + MODEL_AIRPURIFIER_V3, + MODEL_AIRPURIFIER_V5, + MODEL_AIRPURIFIER_PRO, + MODEL_AIRPURIFIER_PRO_V7, + MODEL_AIRPURIFIER_M1, + MODEL_AIRPURIFIER_M2, + MODEL_AIRPURIFIER_MA1, + MODEL_AIRPURIFIER_MA2, + MODEL_AIRPURIFIER_SA1, + MODEL_AIRPURIFIER_SA2, + MODEL_AIRPURIFIER_MC1, + MODEL_AIRHUMIDIFIER_V1, + MODEL_AIRHUMIDIFIER_CA, + MODEL_AIRFRESH_VA2, ]), }) @@ -117,25 +131,41 @@ AVAILABLE_ATTRIBUTES_AIRPURIFIER_COMMON = { ATTR_LED: 'led', ATTR_MOTOR_SPEED: 'motor_speed', ATTR_AVERAGE_AIR_QUALITY_INDEX: 'average_aqi', - ATTR_PURIFY_VOLUME: 'purify_volume', ATTR_LEARN_MODE: 'learn_mode', - ATTR_SLEEP_TIME: 'sleep_time', - ATTR_SLEEP_LEARN_COUNT: 'sleep_mode_learn_count', ATTR_EXTRA_FEATURES: 'extra_features', ATTR_TURBO_MODE_SUPPORTED: 'turbo_mode_supported', - ATTR_AUTO_DETECT: 'auto_detect', - ATTR_USE_TIME: 'use_time', ATTR_BUTTON_PRESSED: 'button_pressed', } AVAILABLE_ATTRIBUTES_AIRPURIFIER = { **AVAILABLE_ATTRIBUTES_AIRPURIFIER_COMMON, + ATTR_PURIFY_VOLUME: 'purify_volume', + ATTR_SLEEP_TIME: 'sleep_time', + ATTR_SLEEP_LEARN_COUNT: 'sleep_mode_learn_count', + ATTR_AUTO_DETECT: 'auto_detect', + ATTR_USE_TIME: 'use_time', ATTR_BUZZER: 'buzzer', ATTR_LED_BRIGHTNESS: 'led_brightness', ATTR_SLEEP_MODE: 'sleep_mode', } AVAILABLE_ATTRIBUTES_AIRPURIFIER_PRO = { + **AVAILABLE_ATTRIBUTES_AIRPURIFIER_COMMON, + ATTR_PURIFY_VOLUME: 'purify_volume', + ATTR_USE_TIME: 'use_time', + ATTR_FILTER_RFID_PRODUCT_ID: 'filter_rfid_product_id', + ATTR_FILTER_RFID_TAG: 'filter_rfid_tag', + ATTR_FILTER_TYPE: 'filter_type', + ATTR_ILLUMINANCE: 'illuminance', + ATTR_MOTOR2_SPEED: 'motor2_speed', + ATTR_VOLUME: 'volume', + # perhaps supported but unconfirmed + ATTR_AUTO_DETECT: 'auto_detect', + ATTR_SLEEP_TIME: 'sleep_time', + ATTR_SLEEP_LEARN_COUNT: 'sleep_mode_learn_count', +} + +AVAILABLE_ATTRIBUTES_AIRPURIFIER_PRO_V7 = { **AVAILABLE_ATTRIBUTES_AIRPURIFIER_COMMON, ATTR_FILTER_RFID_PRODUCT_ID: 'filter_rfid_product_id', ATTR_FILTER_RFID_TAG: 'filter_rfid_tag', @@ -218,6 +248,7 @@ AVAILABLE_ATTRIBUTES_AIRFRESH = { OPERATION_MODES_AIRPURIFIER = ['Auto', 'Silent', 'Favorite', 'Idle'] OPERATION_MODES_AIRPURIFIER_PRO = ['Auto', 'Silent', 'Favorite'] +OPERATION_MODES_AIRPURIFIER_PRO_V7 = OPERATION_MODES_AIRPURIFIER_PRO OPERATION_MODES_AIRPURIFIER_V3 = ['Auto', 'Silent', 'Favorite', 'Idle', 'Medium', 'High', 'Strong'] OPERATION_MODES_AIRFRESH = ['Auto', 'Silent', 'Interval', 'Low', @@ -238,10 +269,8 @@ FEATURE_SET_EXTRA_FEATURES = 512 FEATURE_SET_TARGET_HUMIDITY = 1024 FEATURE_SET_DRY = 2048 -FEATURE_FLAGS_GENERIC = (FEATURE_SET_BUZZER | - FEATURE_SET_CHILD_LOCK) - -FEATURE_FLAGS_AIRPURIFIER = (FEATURE_FLAGS_GENERIC | +FEATURE_FLAGS_AIRPURIFIER = (FEATURE_SET_BUZZER | + FEATURE_SET_CHILD_LOCK | FEATURE_SET_LED | FEATURE_SET_LED_BRIGHTNESS | FEATURE_SET_FAVORITE_LEVEL | @@ -255,17 +284,25 @@ FEATURE_FLAGS_AIRPURIFIER_PRO = (FEATURE_SET_CHILD_LOCK | FEATURE_SET_AUTO_DETECT | FEATURE_SET_VOLUME) -FEATURE_FLAGS_AIRPURIFIER_V3 = (FEATURE_FLAGS_GENERIC | +FEATURE_FLAGS_AIRPURIFIER_PRO_V7 = (FEATURE_SET_CHILD_LOCK | + FEATURE_SET_LED | + FEATURE_SET_FAVORITE_LEVEL | + FEATURE_SET_VOLUME) + +FEATURE_FLAGS_AIRPURIFIER_V3 = (FEATURE_SET_BUZZER | + FEATURE_SET_CHILD_LOCK | FEATURE_SET_LED) -FEATURE_FLAGS_AIRHUMIDIFIER = (FEATURE_FLAGS_GENERIC | +FEATURE_FLAGS_AIRHUMIDIFIER = (FEATURE_SET_BUZZER | + FEATURE_SET_CHILD_LOCK | FEATURE_SET_LED_BRIGHTNESS | FEATURE_SET_TARGET_HUMIDITY) FEATURE_FLAGS_AIRHUMIDIFIER_CA = (FEATURE_FLAGS_AIRHUMIDIFIER | FEATURE_SET_DRY) -FEATURE_FLAGS_AIRFRESH = (FEATURE_FLAGS_GENERIC | +FEATURE_FLAGS_AIRFRESH = (FEATURE_SET_BUZZER | + FEATURE_SET_CHILD_LOCK | FEATURE_SET_LED | FEATURE_SET_LED_BRIGHTNESS | FEATURE_RESET_FILTER | @@ -445,7 +482,7 @@ class XiaomiGenericDevice(FanEntity): self._state_attrs = { ATTR_MODEL: self._model, } - self._device_features = FEATURE_FLAGS_GENERIC + self._device_features = FEATURE_SET_CHILD_LOCK self._skip_update = False @property @@ -577,6 +614,11 @@ class XiaomiAirPurifier(XiaomiGenericDevice): self._device_features = FEATURE_FLAGS_AIRPURIFIER_PRO self._available_attributes = AVAILABLE_ATTRIBUTES_AIRPURIFIER_PRO self._speed_list = OPERATION_MODES_AIRPURIFIER_PRO + elif self._model == MODEL_AIRPURIFIER_PRO_V7: + self._device_features = FEATURE_FLAGS_AIRPURIFIER_PRO_V7 + self._available_attributes = \ + AVAILABLE_ATTRIBUTES_AIRPURIFIER_PRO_V7 + self._speed_list = OPERATION_MODES_AIRPURIFIER_PRO_V7 elif self._model == MODEL_AIRPURIFIER_V3: self._device_features = FEATURE_FLAGS_AIRPURIFIER_V3 self._available_attributes = AVAILABLE_ATTRIBUTES_AIRPURIFIER_V3 From b557157ea1f2322f3ad754d60cbcb4824b8edeb4 Mon Sep 17 00:00:00 2001 From: damarco Date: Thu, 17 Jan 2019 01:09:09 +0100 Subject: [PATCH 209/235] Add support for deconz radios to zha component (#20167) * Add support for deconz radios * Update check_zigpy_connection() --- homeassistant/components/zha/__init__.py | 8 +++++++- homeassistant/components/zha/const.py | 1 + homeassistant/components/zha/helpers.py | 4 ++++ requirements_all.txt | 3 +++ 4 files changed, 15 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index 8ff4c80e0ea..335295b2c2c 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -39,7 +39,8 @@ REQUIREMENTS = [ 'bellows==0.7.0', 'zigpy==0.2.0', 'zigpy-xbee==0.1.1', - 'zha-quirks==0.0.6' + 'zha-quirks==0.0.6', + 'zigpy-deconz==0.0.1' ] DEVICE_CONFIG_SCHEMA_ENTRY = vol.Schema({ @@ -120,6 +121,11 @@ async def async_setup_entry(hass, config_entry): from zigpy_xbee.zigbee.application import ControllerApplication radio = zigpy_xbee.api.XBee() radio_description = "XBee" + elif radio_type == RadioType.deconz.name: + import zigpy_deconz.api + from zigpy_deconz.zigbee.application import ControllerApplication + radio = zigpy_deconz.api.Deconz() + radio_description = "Deconz" await radio.connect(usb_path, baudrate) hass.data[DATA_ZHA][DATA_ZHA_RADIO] = radio diff --git a/homeassistant/components/zha/const.py b/homeassistant/components/zha/const.py index 91a06d666c0..47c3982c5d6 100644 --- a/homeassistant/components/zha/const.py +++ b/homeassistant/components/zha/const.py @@ -57,6 +57,7 @@ class RadioType(enum.Enum): ezsp = 'ezsp' xbee = 'xbee' + deconz = 'deconz' @classmethod def list(cls): diff --git a/homeassistant/components/zha/helpers.py b/homeassistant/components/zha/helpers.py index b6c09dd1fce..a182479d221 100644 --- a/homeassistant/components/zha/helpers.py +++ b/homeassistant/components/zha/helpers.py @@ -118,6 +118,10 @@ async def check_zigpy_connection(usb_path, radio_type, database_path): import zigpy_xbee.api from zigpy_xbee.zigbee.application import ControllerApplication radio = zigpy_xbee.api.XBee() + elif radio_type == RadioType.deconz.name: + import zigpy_deconz.api + from zigpy_deconz.zigbee.application import ControllerApplication + radio = zigpy_deconz.api.Deconz() try: await radio.connect(usb_path, DEFAULT_BAUDRATE) controller = ControllerApplication(radio, database_path) diff --git a/requirements_all.txt b/requirements_all.txt index b6d771900b9..b9e845c34fc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1743,6 +1743,9 @@ zhong_hong_hvac==1.0.9 # homeassistant.components.media_player.ziggo_mediabox_xl ziggo-mediabox-xl==1.1.0 +# homeassistant.components.zha +zigpy-deconz==0.0.1 + # homeassistant.components.zha zigpy-xbee==0.1.1 From bc8aa73448a746329e33956df7c6d37dcd4aada5 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 16 Jan 2019 16:14:47 -0800 Subject: [PATCH 210/235] Updated frontend to 20190116.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 0c2299074d4..a60168ac114 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -24,7 +24,7 @@ from homeassistant.core import callback from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass -REQUIREMENTS = ['home-assistant-frontend==20190115.0'] +REQUIREMENTS = ['home-assistant-frontend==20190116.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log', diff --git a/requirements_all.txt b/requirements_all.txt index b9e845c34fc..cd6c901c4b7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -514,7 +514,7 @@ hole==0.3.0 holidays==0.9.9 # homeassistant.components.frontend -home-assistant-frontend==20190115.0 +home-assistant-frontend==20190116.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 471c3979013..fb49f040b7e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -107,7 +107,7 @@ hdate==0.8.7 holidays==0.9.9 # homeassistant.components.frontend -home-assistant-frontend==20190115.0 +home-assistant-frontend==20190116.0 # homeassistant.components.homematicip_cloud homematicip==0.10.3 From 722d285904b2cb3344772cd23110684fb41d365b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 16 Jan 2019 16:27:15 -0800 Subject: [PATCH 211/235] Bumped version to 0.86.0b0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 370a4d82faf..bb70e635ce3 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 86 -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 d2dec44b18faa6e07a051c68f7756a8ae7cdd73e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 20 Jan 2019 16:22:42 -0800 Subject: [PATCH 212/235] Updated frontend to 20190120.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 a60168ac114..4d18b2fa841 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -24,7 +24,7 @@ from homeassistant.core import callback from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass -REQUIREMENTS = ['home-assistant-frontend==20190116.0'] +REQUIREMENTS = ['home-assistant-frontend==20190120.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log', diff --git a/requirements_all.txt b/requirements_all.txt index cd6c901c4b7..dc14bdf84c5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -514,7 +514,7 @@ hole==0.3.0 holidays==0.9.9 # homeassistant.components.frontend -home-assistant-frontend==20190116.0 +home-assistant-frontend==20190120.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fb49f040b7e..934daa56d98 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -107,7 +107,7 @@ hdate==0.8.7 holidays==0.9.9 # homeassistant.components.frontend -home-assistant-frontend==20190116.0 +home-assistant-frontend==20190120.0 # homeassistant.components.homematicip_cloud homematicip==0.10.3 From 27be95e5978877bb606eeccbef1fa835ef96a296 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 16 Jan 2019 23:12:18 -0800 Subject: [PATCH 213/235] Sensibo to use HA operation modes (#20180) --- homeassistant/components/climate/sensibo.py | 25 ++++++++++++++++----- 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/climate/sensibo.py b/homeassistant/components/climate/sensibo.py index 8532c611d25..bf1cf5bf345 100644 --- a/homeassistant/components/climate/sensibo.py +++ b/homeassistant/components/climate/sensibo.py @@ -19,7 +19,8 @@ from homeassistant.components.climate import ( ATTR_CURRENT_HUMIDITY, ClimateDevice, DOMAIN, PLATFORM_SCHEMA, SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE, SUPPORT_FAN_MODE, SUPPORT_SWING_MODE, - SUPPORT_ON_OFF) + SUPPORT_ON_OFF, STATE_HEAT, STATE_COOL, STATE_FAN_ONLY, STATE_DRY, + STATE_AUTO) from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -57,6 +58,16 @@ FIELD_TO_FLAG = { 'on': SUPPORT_ON_OFF, } +SENSIBO_TO_HA = { + "cool": STATE_COOL, + "heat": STATE_HEAT, + "fan": STATE_FAN_ONLY, + "auto": STATE_AUTO, + "dry": STATE_DRY +} + +HA_TO_SENSIBO = {value: key for key, value in SENSIBO_TO_HA.items()} + async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): @@ -129,9 +140,10 @@ class SensiboClimate(ClimateDevice): self._ac_states = data['acState'] self._status = data['connectionStatus']['isAlive'] capabilities = data['remoteCapabilities'] - self._operations = sorted(capabilities['modes'].keys()) - self._current_capabilities = capabilities[ - 'modes'][self.current_operation] + self._operations = [SENSIBO_TO_HA[mode] for mode + in capabilities['modes']] + self._current_capabilities = \ + capabilities['modes'][self._ac_states['mode']] temperature_unit_key = data.get('temperatureUnit') or \ self._ac_states.get('temperatureUnit') if temperature_unit_key: @@ -186,7 +198,7 @@ class SensiboClimate(ClimateDevice): @property def current_operation(self): """Return current operation ie. heat, cool, idle.""" - return self._ac_states['mode'] + return SENSIBO_TO_HA.get(self._ac_states['mode']) @property def current_humidity(self): @@ -293,7 +305,8 @@ class SensiboClimate(ClimateDevice): """Set new target operation mode.""" with async_timeout.timeout(TIMEOUT): await self._client.async_set_ac_state_property( - self._id, 'mode', operation_mode, self._ac_states) + self._id, 'mode', HA_TO_SENSIBO[operation_mode], + self._ac_states) async def async_set_swing_mode(self, swing_mode): """Set new target swing operation.""" From 1f54edfbc4dc630b88087b4eef0899c88b76d8da Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 17 Jan 2019 10:30:47 -0800 Subject: [PATCH 214/235] Distribute reconnect (#20181) --- homeassistant/components/cloud/iot.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/cloud/iot.py b/homeassistant/components/cloud/iot.py index 7d633a4b2ac..8638a4233ce 100644 --- a/homeassistant/components/cloud/iot.py +++ b/homeassistant/components/cloud/iot.py @@ -2,6 +2,7 @@ import asyncio import logging import pprint +import random import uuid from aiohttp import hdrs, client_exceptions, WSMsgType @@ -107,9 +108,11 @@ class CloudIoT: self.tries += 1 try: - # Sleep 2^tries seconds between retries - self.retry_task = hass.async_create_task(asyncio.sleep( - 2**min(9, self.tries), loop=hass.loop)) + # Sleep 2^tries + 0…tries*3 seconds between retries + self.retry_task = hass.async_create_task( + asyncio.sleep(2**min(9, self.tries) + + random.randint(0, self.tries * 3), + loop=hass.loop)) yield from self.retry_task self.retry_task = None except asyncio.CancelledError: From 84d6453a9774587f2e1e6956d6f33be89e30fb75 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 17 Jan 2019 10:33:01 -0800 Subject: [PATCH 215/235] Add command to refresh auth (#20183) --- homeassistant/components/cloud/iot.py | 11 ++++++++--- tests/components/cloud/test_iot.py | 28 +++++++++++++++++++++++---- 2 files changed, 32 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/cloud/iot.py b/homeassistant/components/cloud/iot.py index 8638a4233ce..ed24fe48d40 100644 --- a/homeassistant/components/cloud/iot.py +++ b/homeassistant/components/cloud/iot.py @@ -316,15 +316,20 @@ def async_handle_google_actions(hass, cloud, payload): @HANDLERS.register('cloud') -@asyncio.coroutine -def async_handle_cloud(hass, cloud, payload): +async def async_handle_cloud(hass, cloud, payload): """Handle an incoming IoT message for cloud component.""" action = payload['action'] if action == 'logout': - yield from cloud.logout() + # Log out of Home Assistant Cloud + await cloud.logout() _LOGGER.error("You have been logged out from Home Assistant cloud: %s", payload['reason']) + elif action == 'refresh_auth': + # Refresh the auth token between now and payload['seconds'] + hass.helpers.event.async_call_later( + random.randint(0, payload['seconds']), + lambda now: auth_api.check_token(cloud)) else: _LOGGER.warning("Received unknown cloud action: %s", action) diff --git a/tests/components/cloud/test_iot.py b/tests/components/cloud/test_iot.py index 2133a803aef..1a528f8cedf 100644 --- a/tests/components/cloud/test_iot.py +++ b/tests/components/cloud/test_iot.py @@ -10,8 +10,9 @@ from homeassistant.components.cloud import ( Cloud, iot, auth_api, MODE_DEV) from homeassistant.components.cloud.const import ( PREF_ENABLE_ALEXA, PREF_ENABLE_GOOGLE) +from homeassistant.util import dt as dt_util from tests.components.alexa import test_smart_home as test_alexa -from tests.common import mock_coro +from tests.common import mock_coro, async_fire_time_changed from . import mock_cloud_prefs @@ -147,17 +148,36 @@ def test_handler_forwarding(): assert payload == 'payload' -@asyncio.coroutine -def test_handling_core_messages(hass, mock_cloud): +async def test_handling_core_messages_logout(hass, mock_cloud): """Test handling core messages.""" mock_cloud.logout.return_value = mock_coro() - yield from iot.async_handle_cloud(hass, mock_cloud, { + await iot.async_handle_cloud(hass, mock_cloud, { 'action': 'logout', 'reason': 'Logged in at two places.' }) assert len(mock_cloud.logout.mock_calls) == 1 +async def test_handling_core_messages_refresh_auth(hass, mock_cloud): + """Test handling core messages.""" + mock_cloud.hass = hass + with patch('random.randint', return_value=0) as mock_rand, patch( + 'homeassistant.components.cloud.auth_api.check_token' + ) as mock_check: + await iot.async_handle_cloud(hass, mock_cloud, { + 'action': 'refresh_auth', + 'seconds': 230, + }) + async_fire_time_changed(hass, dt_util.utcnow()) + await hass.async_block_till_done() + + assert len(mock_rand.mock_calls) == 1 + assert mock_rand.mock_calls[0][1] == (0, 230) + + assert len(mock_check.mock_calls) == 1 + assert mock_check.mock_calls[0][1][0] is mock_cloud + + @asyncio.coroutine def test_cloud_getting_disconnected_by_server(mock_client, caplog, mock_cloud): """Test server disconnecting instance.""" From d843bf9c58979cf906da37aaf07a76e05fa8665d Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Fri, 18 Jan 2019 13:43:48 +0100 Subject: [PATCH 216/235] Improve Sonos discovery (#20196) --- homeassistant/components/sonos/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sonos/__init__.py b/homeassistant/components/sonos/__init__.py index 529df41de58..b4f507a60dd 100644 --- a/homeassistant/components/sonos/__init__.py +++ b/homeassistant/components/sonos/__init__.py @@ -4,7 +4,7 @@ from homeassistant.helpers import config_entry_flow DOMAIN = 'sonos' -REQUIREMENTS = ['pysonos==0.0.5'] +REQUIREMENTS = ['pysonos==0.0.6'] async def async_setup(hass, config): diff --git a/requirements_all.txt b/requirements_all.txt index dc14bdf84c5..31202d3a6ad 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1208,7 +1208,7 @@ pysma==0.3.1 pysnmp==4.4.8 # homeassistant.components.sonos -pysonos==0.0.5 +pysonos==0.0.6 # homeassistant.components.spc pyspcwebgw==0.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 934daa56d98..5733d7e9630 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -202,7 +202,7 @@ pyotp==2.2.6 pyqwikswitch==0.8 # homeassistant.components.sonos -pysonos==0.0.5 +pysonos==0.0.6 # homeassistant.components.spc pyspcwebgw==0.4.0 From ba2b28cd4dcfc9d6c816ff54af11baba05cc74d9 Mon Sep 17 00:00:00 2001 From: Phil Bruckner Date: Sun, 20 Jan 2019 18:46:14 -0600 Subject: [PATCH 217/235] Handle non-string values in JSON renderer (#20233) Handle the case of async_render_with_possible_json_value's value argument being something other than a string. This can happen, e.g., when using the SQL sensor to extract a datetime column such as last_changed and also using its value_template to convert that datetime to another format. This was causing a TypeError from json.loads, but async_render_with_possible_json_value was only catching ValueError's. --- homeassistant/helpers/template.py | 2 +- tests/helpers/test_template.py | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index e82302dfd3b..d8bbcbc6e12 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -165,7 +165,7 @@ class Template: try: variables['value_json'] = json.loads(value) - except ValueError: + except (ValueError, TypeError): pass try: diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index 02331c400d3..3febd4037ad 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -4,6 +4,7 @@ from datetime import datetime import unittest import random import math +import pytz from unittest.mock import patch from homeassistant.components import group @@ -422,6 +423,16 @@ class TestHelpersTemplate(unittest.TestCase): assert '' == \ tpl.render_with_possible_json_value('{"hello": "world"}', '') + def test_render_with_possible_json_value_non_string_value(self): + """Render with possible JSON value with non-string value.""" + tpl = template.Template(""" +{{ strptime(value~'+0000', '%Y-%m-%d %H:%M:%S%z') }} + """, self.hass) + value = datetime(2019, 1, 18, 12, 13, 14) + expected = str(pytz.utc.localize(value)) + assert expected == \ + tpl.render_with_possible_json_value(value) + def test_raise_exception_on_error(self): """Test raising an exception on error.""" with pytest.raises(TemplateError): From 33ed113211f5cd35abe272057dbf9ba735d80239 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Sat, 19 Jan 2019 17:13:32 +0100 Subject: [PATCH 218/235] Bump aioesphomeapi to 1.4.2 (#20247) * Bump aioesphomeapi to 1.4.2 * Update requirements_all.txt --- homeassistant/components/esphome/__init__.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index 7578f2e244f..1ff2c10c828 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -31,7 +31,7 @@ if TYPE_CHECKING: ServiceCall DOMAIN = 'esphome' -REQUIREMENTS = ['aioesphomeapi==1.4.1'] +REQUIREMENTS = ['aioesphomeapi==1.4.2'] DISPATCHER_UPDATE_ENTITY = 'esphome_{entry_id}_update_{component_key}_{key}' diff --git a/requirements_all.txt b/requirements_all.txt index 31202d3a6ad..cbeb6732c02 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -96,7 +96,7 @@ aioautomatic==0.6.5 aiodns==1.1.1 # homeassistant.components.esphome -aioesphomeapi==1.4.1 +aioesphomeapi==1.4.2 # homeassistant.components.freebox aiofreepybox==0.0.6 From 97e8e20bcc46857af84541f38098674898050e29 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Mon, 21 Jan 2019 00:10:12 +0100 Subject: [PATCH 219/235] Remove double logging of automation action (#20264) --- homeassistant/components/automation/__init__.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 6c9b04f9fa2..836901cde30 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -375,8 +375,6 @@ def _async_get_action(hass, config, name): async def action(entity_id, variables, context): """Execute an action.""" _LOGGER.info('Executing %s', name) - hass.components.logbook.async_log_entry( - name, 'has been triggered', DOMAIN, entity_id) try: await script_obj.async_run(variables, context) From f02e887fccd6b870df64b1b7ef0e6413ec1fbd28 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Mon, 21 Jan 2019 01:33:39 +0100 Subject: [PATCH 220/235] Allow 'all' entity_id in service schema (#20278) --- homeassistant/helpers/config_validation.py | 2 +- tests/helpers/test_config_validation.py | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 245cc5d46bd..92fe935085a 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -517,7 +517,7 @@ SERVICE_SCHEMA = vol.All(vol.Schema({ vol.Exclusive('service_template', 'service name'): template, vol.Optional('data'): dict, vol.Optional('data_template'): {match_all: template_complex}, - vol.Optional(CONF_ENTITY_ID): entity_ids, + vol.Optional(CONF_ENTITY_ID): comp_entity_ids, }), has_at_least_one_key('service', 'service_template')) NUMERIC_STATE_CONDITION_SCHEMA = vol.All(vol.Schema({ diff --git a/tests/helpers/test_config_validation.py b/tests/helpers/test_config_validation.py index 791570981e2..03dd3cfe55a 100644 --- a/tests/helpers/test_config_validation.py +++ b/tests/helpers/test_config_validation.py @@ -332,6 +332,10 @@ def test_service_schema(): 'service': 'homeassistant.turn_on', 'entity_id': 'light.kitchen', }, + { + 'service': 'light.turn_on', + 'entity_id': 'all', + }, { 'service': 'homeassistant.turn_on', 'entity_id': ['light.kitchen', 'light.ceiling'], From 41d232175612f1f20b6c811ff15fa5e7d0ea9c3e Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Mon, 21 Jan 2019 01:33:11 +0100 Subject: [PATCH 221/235] Fix 'all' entity_id in service call extraction (#20281) --- homeassistant/helpers/entity_component.py | 5 +++-- tests/helpers/test_entity_component.py | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py index ce876991097..21634121cd2 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -7,7 +7,8 @@ import logging from homeassistant import config as conf_util from homeassistant.setup import async_prepare_setup_platform from homeassistant.const import ( - ATTR_ENTITY_ID, CONF_SCAN_INTERVAL, CONF_ENTITY_NAMESPACE, MATCH_ALL) + ATTR_ENTITY_ID, CONF_SCAN_INTERVAL, CONF_ENTITY_NAMESPACE, + ENTITY_MATCH_ALL) from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_per_platform, discovery @@ -163,7 +164,7 @@ class EntityComponent: """ data_ent_id = service.data.get(ATTR_ENTITY_ID) - if data_ent_id in (None, MATCH_ALL): + if data_ent_id in (None, ENTITY_MATCH_ALL): if data_ent_id is None: self.logger.warning( 'Not passing an entity ID to a service to target all ' diff --git a/tests/helpers/test_entity_component.py b/tests/helpers/test_entity_component.py index 8f54f0ee5bc..27e33a4fe7d 100644 --- a/tests/helpers/test_entity_component.py +++ b/tests/helpers/test_entity_component.py @@ -479,7 +479,7 @@ async def test_extract_all_use_match_all(hass, caplog): MockEntity(name='test_2'), ]) - call = ha.ServiceCall('test', 'service', {'entity_id': '*'}) + call = ha.ServiceCall('test', 'service', {'entity_id': 'all'}) assert ['test_domain.test_1', 'test_domain.test_2'] == \ sorted(ent.entity_id for ent in From 143eb4e8f42e44379c9d8623e4add0f43b4f010d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 20 Jan 2019 17:02:44 -0800 Subject: [PATCH 222/235] Bumped version to 0.86.0b1 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index bb70e635ce3..6cd6bb21940 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 86 -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 657544a381ac2e9d40bd439514249b6a1c75d4cc Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 20 Jan 2019 17:31:09 -0800 Subject: [PATCH 223/235] Clean up build artifacts correctly --- script/release | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/release b/script/release index cf4f808377e..4dc94eb7f15 100755 --- a/script/release +++ b/script/release @@ -27,6 +27,6 @@ then exit 1 fi -rm -rf dist +rm -rf dist build python3 setup.py sdist bdist_wheel python3 -m twine upload dist/* --skip-existing From df47a8c58cadba1787142a5d9959e9a27856eb3e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 21 Jan 2019 09:21:11 -0800 Subject: [PATCH 224/235] Updated frontend to 20190121.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 4d18b2fa841..d7b77fa903f 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -24,7 +24,7 @@ from homeassistant.core import callback from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass -REQUIREMENTS = ['home-assistant-frontend==20190120.0'] +REQUIREMENTS = ['home-assistant-frontend==20190121.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log', diff --git a/requirements_all.txt b/requirements_all.txt index cbeb6732c02..4d3fc03f89d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -514,7 +514,7 @@ hole==0.3.0 holidays==0.9.9 # homeassistant.components.frontend -home-assistant-frontend==20190120.0 +home-assistant-frontend==20190121.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5733d7e9630..8b24a7b2466 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -107,7 +107,7 @@ hdate==0.8.7 holidays==0.9.9 # homeassistant.components.frontend -home-assistant-frontend==20190120.0 +home-assistant-frontend==20190121.0 # homeassistant.components.homematicip_cloud homematicip==0.10.3 From 028cc8d24f327f0f6c56c71589df24bfd58c3557 Mon Sep 17 00:00:00 2001 From: Johann Kellerman Date: Mon, 21 Jan 2019 19:45:11 +0200 Subject: [PATCH 225/235] Align valid_entity_id with new slugify (#20231) * slug * ensure a dot * fix * schema_with_slug_keys * lint * test --- homeassistant/components/script.py | 2 +- homeassistant/config.py | 9 ++++----- homeassistant/core.py | 14 +++++++------- homeassistant/helpers/config_validation.py | 20 ++++++++++++++++++-- tests/components/switch/test_wake_on_lan.py | 4 ++-- 5 files changed, 32 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/script.py b/homeassistant/components/script.py index 54490af3cfa..15df6907468 100644 --- a/homeassistant/components/script.py +++ b/homeassistant/components/script.py @@ -45,7 +45,7 @@ SCRIPT_ENTRY_SCHEMA = vol.Schema({ }) CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({cv.slug: SCRIPT_ENTRY_SCHEMA}) + DOMAIN: cv.schema_with_slug_keys(SCRIPT_ENTRY_SCHEMA) }, extra=vol.ALLOW_EXTRA) SCRIPT_SERVICE_SCHEMA = vol.Schema(dict) diff --git a/homeassistant/config.py b/homeassistant/config.py index 10d3ce21a00..0edadf6a78d 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -170,10 +170,9 @@ def _no_duplicate_auth_mfa_module(configs: Sequence[Dict[str, Any]]) \ return configs -PACKAGES_CONFIG_SCHEMA = vol.Schema({ - cv.slug: vol.Schema( # Package names are slugs - {cv.string: vol.Any(dict, list, None)}) # Component configuration -}) +PACKAGES_CONFIG_SCHEMA = cv.schema_with_slug_keys( # Package names are slugs + vol.Schema({cv.string: vol.Any(dict, list, None)}) # Component config +) CUSTOMIZE_DICT_SCHEMA = vol.Schema({ vol.Optional(ATTR_FRIENDLY_NAME): cv.string, @@ -627,7 +626,7 @@ def _identify_config_schema(module: ModuleType) -> \ except (AttributeError, KeyError): return None, None t_schema = str(schema) - if t_schema.startswith('{'): + if t_schema.startswith('{') or 'schema_with_slug_keys' in t_schema: return ('dict', schema) if t_schema.startswith(('[', 'All(.) -ENTITY_ID_PATTERN = re.compile(r"^(\w+)\.(\w+)$") - # How long to wait till things that run on startup have to finish. TIMEOUT_EVENT_START = 15 @@ -77,8 +73,12 @@ def split_entity_id(entity_id: str) -> List[str]: def valid_entity_id(entity_id: str) -> bool: - """Test if an entity ID is a valid format.""" - return ENTITY_ID_PATTERN.match(entity_id) is not None + """Test if an entity ID is a valid format. + + Format: . where both are slugs. + """ + return ('.' in entity_id and + slugify(entity_id) == entity_id.replace('.', '_', 1)) def valid_state(state: str) -> bool: diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 92fe935085a..ef0166bc16d 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -319,7 +319,23 @@ def service(value): .format(value)) -def slug(value): +def schema_with_slug_keys(value_schema: Union[T, Callable]) -> Callable: + """Ensure dicts have slugs as keys. + + Replacement of vol.Schema({cv.slug: value_schema}) to prevent misleading + "Extra keys" errors from voluptuous. + """ + schema = vol.Schema({str: value_schema}) + + def verify(value: Dict) -> Dict: + """Validate all keys are slugs and then the value_schema.""" + for key in value.keys(): + slug(key) + return schema(value) + return verify + + +def slug(value: Any) -> str: """Validate value is a valid slug.""" if value is None: raise vol.Invalid('Slug should not be None') @@ -330,7 +346,7 @@ def slug(value): raise vol.Invalid('invalid slug {} (try {})'.format(value, slg)) -def slugify(value): +def slugify(value: Any) -> str: """Coerce a value to a slug.""" if value is None: raise vol.Invalid('Slug should not be None') diff --git a/tests/components/switch/test_wake_on_lan.py b/tests/components/switch/test_wake_on_lan.py index c3f4e04057d..312a49b5183 100644 --- a/tests/components/switch/test_wake_on_lan.py +++ b/tests/components/switch/test_wake_on_lan.py @@ -139,11 +139,11 @@ class TestWOLSwitch(unittest.TestCase): 'mac_address': '00-01-02-03-04-05', 'host': 'validhostname', 'turn_off': { - 'service': 'shell_command.turn_off_TARGET', + 'service': 'shell_command.turn_off_target', }, } }) - calls = mock_service(self.hass, 'shell_command', 'turn_off_TARGET') + calls = mock_service(self.hass, 'shell_command', 'turn_off_target') state = self.hass.states.get('switch.wake_on_lan') assert STATE_OFF == state.state From 9667c8057f496b63ceb0de8ad35be55ded99329b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 21 Jan 2019 09:45:42 -0800 Subject: [PATCH 226/235] Bumped version to 0.86.0b2 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 6cd6bb21940..36d66939e78 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 86 -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 5945929e7e4555fbb51742e518cc1c049f0b9264 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 21 Jan 2019 21:20:08 -0800 Subject: [PATCH 227/235] Updated frontend to 20190121.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 d7b77fa903f..f5cc33b63a0 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -24,7 +24,7 @@ from homeassistant.core import callback from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass -REQUIREMENTS = ['home-assistant-frontend==20190121.0'] +REQUIREMENTS = ['home-assistant-frontend==20190121.1'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log', diff --git a/requirements_all.txt b/requirements_all.txt index 4d3fc03f89d..7a3cca7a7ee 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -514,7 +514,7 @@ hole==0.3.0 holidays==0.9.9 # homeassistant.components.frontend -home-assistant-frontend==20190121.0 +home-assistant-frontend==20190121.1 # homeassistant.components.zwave homeassistant-pyozw==0.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8b24a7b2466..edcd1d101aa 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -107,7 +107,7 @@ hdate==0.8.7 holidays==0.9.9 # homeassistant.components.frontend -home-assistant-frontend==20190121.0 +home-assistant-frontend==20190121.1 # homeassistant.components.homematicip_cloud homematicip==0.10.3 From ec7f2657cda8035a23aeac3644a7030735056033 Mon Sep 17 00:00:00 2001 From: Johann Kellerman Date: Tue, 22 Jan 2019 02:36:04 +0200 Subject: [PATCH 228/235] Config Validator: schema_with_slug_keys (#20298) * schema_with_slug_keys * Update config_validation.py * Update config_validation.py --- homeassistant/components/alert.py | 4 +--- homeassistant/components/axis/__init__.py | 4 +--- homeassistant/components/binary_sensor/template.py | 2 +- homeassistant/components/binary_sensor/trend.py | 2 +- homeassistant/components/counter/__init__.py | 6 +++--- homeassistant/components/cover/command_line.py | 2 +- homeassistant/components/cover/garadget.py | 2 +- homeassistant/components/cover/opengarage.py | 2 +- homeassistant/components/cover/scsgate.py | 3 ++- homeassistant/components/cover/template.py | 2 +- homeassistant/components/cover/velbus.py | 2 +- homeassistant/components/fan/template.py | 2 +- homeassistant/components/history_graph.py | 2 +- homeassistant/components/input_boolean.py | 6 +++--- homeassistant/components/input_datetime.py | 7 ++++--- homeassistant/components/input_number.py | 6 +++--- homeassistant/components/input_select.py | 7 ++++--- homeassistant/components/input_text.py | 6 +++--- homeassistant/components/light/scsgate.py | 3 ++- homeassistant/components/light/template.py | 2 +- homeassistant/components/media_player/universal.py | 4 ++-- homeassistant/components/panel_iframe.py | 8 +++++--- homeassistant/components/proximity.py | 4 +--- homeassistant/components/remote/xiaomi_miio.py | 2 +- homeassistant/components/rest_command.py | 4 +--- homeassistant/components/sensor/lacrosse.py | 2 +- homeassistant/components/sensor/sma.py | 4 ++-- homeassistant/components/sensor/template.py | 2 +- homeassistant/components/shell_command.py | 4 +--- homeassistant/components/switch/broadlink.py | 2 +- homeassistant/components/switch/command_line.py | 2 +- homeassistant/components/switch/kankun.py | 2 +- homeassistant/components/switch/scsgate.py | 3 ++- homeassistant/components/switch/telnet.py | 2 +- homeassistant/components/switch/template.py | 2 +- homeassistant/components/timer/__init__.py | 6 +++--- homeassistant/components/volvooncall.py | 4 ++-- homeassistant/helpers/config_validation.py | 3 +++ 38 files changed, 66 insertions(+), 66 deletions(-) diff --git a/homeassistant/components/alert.py b/homeassistant/components/alert.py index 759a2185047..3a18281e49b 100644 --- a/homeassistant/components/alert.py +++ b/homeassistant/components/alert.py @@ -46,9 +46,7 @@ ALERT_SCHEMA = vol.Schema({ vol.Required(CONF_NOTIFIERS): cv.ensure_list}) CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - cv.slug: ALERT_SCHEMA, - }), + DOMAIN: cv.schema_with_slug_keys(ALERT_SCHEMA), }, extra=vol.ALLOW_EXTRA) diff --git a/homeassistant/components/axis/__init__.py b/homeassistant/components/axis/__init__.py index 26fe41724f9..fd2e603445c 100644 --- a/homeassistant/components/axis/__init__.py +++ b/homeassistant/components/axis/__init__.py @@ -50,9 +50,7 @@ DEVICE_SCHEMA = vol.Schema({ }) CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - cv.slug: DEVICE_SCHEMA, - }), + DOMAIN: cv.schema_with_slug_keys(DEVICE_SCHEMA), }, extra=vol.ALLOW_EXTRA) SERVICE_VAPIX_CALL = 'vapix_call' diff --git a/homeassistant/components/binary_sensor/template.py b/homeassistant/components/binary_sensor/template.py index d5f8b16e0c1..605ab24a264 100644 --- a/homeassistant/components/binary_sensor/template.py +++ b/homeassistant/components/binary_sensor/template.py @@ -41,7 +41,7 @@ SENSOR_SCHEMA = vol.Schema({ }) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_SENSORS): vol.Schema({cv.slug: SENSOR_SCHEMA}), + vol.Required(CONF_SENSORS): cv.schema_with_slug_keys(SENSOR_SCHEMA), }) diff --git a/homeassistant/components/binary_sensor/trend.py b/homeassistant/components/binary_sensor/trend.py index 4773e88f5df..8f3ff5d798e 100644 --- a/homeassistant/components/binary_sensor/trend.py +++ b/homeassistant/components/binary_sensor/trend.py @@ -51,7 +51,7 @@ SENSOR_SCHEMA = vol.Schema({ }) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_SENSORS): vol.Schema({cv.slug: SENSOR_SCHEMA}), + vol.Required(CONF_SENSORS): cv.schema_with_slug_keys(SENSOR_SCHEMA), }) diff --git a/homeassistant/components/counter/__init__.py b/homeassistant/components/counter/__init__.py index cd3a29df2b6..aeef2818f63 100644 --- a/homeassistant/components/counter/__init__.py +++ b/homeassistant/components/counter/__init__.py @@ -37,8 +37,8 @@ SERVICE_SCHEMA = vol.Schema({ }) CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - cv.slug: vol.Any({ + DOMAIN: cv.schema_with_slug_keys( + vol.Any({ vol.Optional(CONF_ICON): cv.icon, vol.Optional(CONF_INITIAL, default=DEFAULT_INITIAL): cv.positive_int, @@ -46,7 +46,7 @@ CONFIG_SCHEMA = vol.Schema({ vol.Optional(CONF_RESTORE, default=True): cv.boolean, vol.Optional(CONF_STEP, default=DEFAULT_STEP): cv.positive_int, }, None) - }) + ) }, extra=vol.ALLOW_EXTRA) diff --git a/homeassistant/components/cover/command_line.py b/homeassistant/components/cover/command_line.py index bebf78b1db6..4f4fca1b27a 100644 --- a/homeassistant/components/cover/command_line.py +++ b/homeassistant/components/cover/command_line.py @@ -27,7 +27,7 @@ COVER_SCHEMA = vol.Schema({ }) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_COVERS): vol.Schema({cv.slug: COVER_SCHEMA}), + vol.Required(CONF_COVERS): cv.schema_with_slug_keys(COVER_SCHEMA), }) diff --git a/homeassistant/components/cover/garadget.py b/homeassistant/components/cover/garadget.py index 03756a971bc..28be3dc6b82 100644 --- a/homeassistant/components/cover/garadget.py +++ b/homeassistant/components/cover/garadget.py @@ -47,7 +47,7 @@ COVER_SCHEMA = vol.Schema({ }) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_COVERS): vol.Schema({cv.slug: COVER_SCHEMA}), + vol.Required(CONF_COVERS): cv.schema_with_slug_keys(COVER_SCHEMA), }) diff --git a/homeassistant/components/cover/opengarage.py b/homeassistant/components/cover/opengarage.py index 19a87c5bf7c..664d2e291ac 100644 --- a/homeassistant/components/cover/opengarage.py +++ b/homeassistant/components/cover/opengarage.py @@ -46,7 +46,7 @@ COVER_SCHEMA = vol.Schema({ }) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_COVERS): vol.Schema({cv.slug: COVER_SCHEMA}), + vol.Required(CONF_COVERS): cv.schema_with_slug_keys(COVER_SCHEMA), }) diff --git a/homeassistant/components/cover/scsgate.py b/homeassistant/components/cover/scsgate.py index a6f09c7237d..2d85c1fe3c3 100644 --- a/homeassistant/components/cover/scsgate.py +++ b/homeassistant/components/cover/scsgate.py @@ -18,7 +18,8 @@ _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['scsgate'] PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_DEVICES): vol.Schema({cv.slug: scsgate.SCSGATE_SCHEMA}), + vol.Required(CONF_DEVICES): + cv.schema_with_slug_keys(scsgate.SCSGATE_SCHEMA), }) diff --git a/homeassistant/components/cover/template.py b/homeassistant/components/cover/template.py index f64e4ae7a3f..1d3642a6036 100644 --- a/homeassistant/components/cover/template.py +++ b/homeassistant/components/cover/template.py @@ -67,7 +67,7 @@ COVER_SCHEMA = vol.Schema({ }) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_COVERS): vol.Schema({cv.slug: COVER_SCHEMA}), + vol.Required(CONF_COVERS): cv.schema_with_slug_keys(COVER_SCHEMA), }) diff --git a/homeassistant/components/cover/velbus.py b/homeassistant/components/cover/velbus.py index a8501778884..7e5099cecf8 100644 --- a/homeassistant/components/cover/velbus.py +++ b/homeassistant/components/cover/velbus.py @@ -26,7 +26,7 @@ COVER_SCHEMA = vol.Schema({ }) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_COVERS): vol.Schema({cv.slug: COVER_SCHEMA}), + vol.Required(CONF_COVERS): cv.schema_with_slug_keys(COVER_SCHEMA), }) DEPENDENCIES = ['velbus'] diff --git a/homeassistant/components/fan/template.py b/homeassistant/components/fan/template.py index a2f33d40e48..d9182b79a40 100644 --- a/homeassistant/components/fan/template.py +++ b/homeassistant/components/fan/template.py @@ -62,7 +62,7 @@ FAN_SCHEMA = vol.Schema({ }) PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend({ - vol.Required(CONF_FANS): vol.Schema({cv.slug: FAN_SCHEMA}), + vol.Required(CONF_FANS): cv.schema_with_slug_keys(FAN_SCHEMA), }) diff --git a/homeassistant/components/history_graph.py b/homeassistant/components/history_graph.py index fa7d615dce2..7d9db379705 100644 --- a/homeassistant/components/history_graph.py +++ b/homeassistant/components/history_graph.py @@ -34,7 +34,7 @@ GRAPH_SCHEMA = vol.Schema({ CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({cv.slug: GRAPH_SCHEMA}) + DOMAIN: cv.schema_with_slug_keys(GRAPH_SCHEMA) }, extra=vol.ALLOW_EXTRA) diff --git a/homeassistant/components/input_boolean.py b/homeassistant/components/input_boolean.py index 541e38202fc..896de61130c 100644 --- a/homeassistant/components/input_boolean.py +++ b/homeassistant/components/input_boolean.py @@ -30,13 +30,13 @@ SERVICE_SCHEMA = vol.Schema({ }) CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - cv.slug: vol.Any({ + DOMAIN: cv.schema_with_slug_keys( + vol.Any({ vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_INITIAL): cv.boolean, vol.Optional(CONF_ICON): cv.icon, }, None) - }) + ) }, extra=vol.ALLOW_EXTRA) diff --git a/homeassistant/components/input_datetime.py b/homeassistant/components/input_datetime.py index 6ac9a24d044..63dcc364c9c 100644 --- a/homeassistant/components/input_datetime.py +++ b/homeassistant/components/input_datetime.py @@ -46,14 +46,15 @@ def has_date_or_time(conf): CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - cv.slug: vol.All({ + DOMAIN: cv.schema_with_slug_keys( + vol.All({ vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_HAS_DATE, default=False): cv.boolean, vol.Optional(CONF_HAS_TIME, default=False): cv.boolean, vol.Optional(CONF_ICON): cv.icon, vol.Optional(CONF_INITIAL): cv.string, - }, has_date_or_time)}) + }, has_date_or_time) + ) }, extra=vol.ALLOW_EXTRA) diff --git a/homeassistant/components/input_number.py b/homeassistant/components/input_number.py index b6c6eab3cf5..8cfa7abaf20 100644 --- a/homeassistant/components/input_number.py +++ b/homeassistant/components/input_number.py @@ -63,8 +63,8 @@ def _cv_input_number(cfg): CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - cv.slug: vol.All({ + DOMAIN: cv.schema_with_slug_keys( + vol.All({ vol.Optional(CONF_NAME): cv.string, vol.Required(CONF_MIN): vol.Coerce(float), vol.Required(CONF_MAX): vol.Coerce(float), @@ -76,7 +76,7 @@ CONFIG_SCHEMA = vol.Schema({ vol.Optional(CONF_MODE, default=MODE_SLIDER): vol.In([MODE_BOX, MODE_SLIDER]), }, _cv_input_number) - }) + ) }, required=True, extra=vol.ALLOW_EXTRA) diff --git a/homeassistant/components/input_select.py b/homeassistant/components/input_select.py index cc9a73bf915..fc858e75397 100644 --- a/homeassistant/components/input_select.py +++ b/homeassistant/components/input_select.py @@ -64,14 +64,15 @@ def _cv_input_select(cfg): CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - cv.slug: vol.All({ + DOMAIN: cv.schema_with_slug_keys( + vol.All({ vol.Optional(CONF_NAME): cv.string, vol.Required(CONF_OPTIONS): vol.All(cv.ensure_list, vol.Length(min=1), [cv.string]), vol.Optional(CONF_INITIAL): cv.string, vol.Optional(CONF_ICON): cv.icon, - }, _cv_input_select)}) + }, _cv_input_select) + ) }, required=True, extra=vol.ALLOW_EXTRA) diff --git a/homeassistant/components/input_text.py b/homeassistant/components/input_text.py index 8ac64b398f4..580337a3af3 100644 --- a/homeassistant/components/input_text.py +++ b/homeassistant/components/input_text.py @@ -55,8 +55,8 @@ def _cv_input_text(cfg): CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - cv.slug: vol.All({ + DOMAIN: cv.schema_with_slug_keys( + vol.All({ vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_MIN, default=0): vol.Coerce(int), vol.Optional(CONF_MAX, default=100): vol.Coerce(int), @@ -67,7 +67,7 @@ CONFIG_SCHEMA = vol.Schema({ vol.Optional(CONF_MODE, default=MODE_TEXT): vol.In([MODE_TEXT, MODE_PASSWORD]), }, _cv_input_text) - }) + ) }, required=True, extra=vol.ALLOW_EXTRA) diff --git a/homeassistant/components/light/scsgate.py b/homeassistant/components/light/scsgate.py index 4a18bc99672..c218e194791 100644 --- a/homeassistant/components/light/scsgate.py +++ b/homeassistant/components/light/scsgate.py @@ -19,7 +19,8 @@ _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['scsgate'] PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_DEVICES): vol.Schema({cv.slug: scsgate.SCSGATE_SCHEMA}), + vol.Required(CONF_DEVICES): + cv.schema_with_slug_keys(scsgate.SCSGATE_SCHEMA), }) diff --git a/homeassistant/components/light/template.py b/homeassistant/components/light/template.py index 2447dabe3c7..bf930dd1b38 100644 --- a/homeassistant/components/light/template.py +++ b/homeassistant/components/light/template.py @@ -44,7 +44,7 @@ LIGHT_SCHEMA = vol.Schema({ }) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_LIGHTS): vol.Schema({cv.slug: LIGHT_SCHEMA}), + vol.Required(CONF_LIGHTS): cv.schema_with_slug_keys(LIGHT_SCHEMA), }) diff --git a/homeassistant/components/media_player/universal.py b/homeassistant/components/media_player/universal.py index 47eaf599929..18b953a0372 100644 --- a/homeassistant/components/media_player/universal.py +++ b/homeassistant/components/media_player/universal.py @@ -47,8 +47,8 @@ CONF_SERVICE_DATA = 'service_data' OFF_STATES = [STATE_IDLE, STATE_OFF, STATE_UNAVAILABLE] -ATTRS_SCHEMA = vol.Schema({cv.slug: cv.string}) -CMD_SCHEMA = vol.Schema({cv.slug: cv.SERVICE_SCHEMA}) +ATTRS_SCHEMA = cv.schema_with_slug_keys(cv.string) +CMD_SCHEMA = cv.schema_with_slug_keys(cv.SERVICE_SCHEMA) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_NAME): cv.string, diff --git a/homeassistant/components/panel_iframe.py b/homeassistant/components/panel_iframe.py index 86594b74995..030fbbf9324 100644 --- a/homeassistant/components/panel_iframe.py +++ b/homeassistant/components/panel_iframe.py @@ -19,8 +19,8 @@ CONF_RELATIVE_URL_ERROR_MSG = "Invalid relative URL. Absolute path required." CONF_RELATIVE_URL_REGEX = r'\A/' CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - cv.slug: { + DOMAIN: cv.schema_with_slug_keys( + vol.Schema({ # pylint: disable=no-value-for-parameter vol.Optional(CONF_TITLE): cv.string, vol.Optional(CONF_ICON): cv.icon, @@ -29,7 +29,9 @@ CONFIG_SCHEMA = vol.Schema({ CONF_RELATIVE_URL_REGEX, msg=CONF_RELATIVE_URL_ERROR_MSG), vol.Url()), - }})}, extra=vol.ALLOW_EXTRA) + }) + ) +}, extra=vol.ALLOW_EXTRA) async def async_setup(hass, config): diff --git a/homeassistant/components/proximity.py b/homeassistant/components/proximity.py index 38b37cad51e..e8d86d480e5 100644 --- a/homeassistant/components/proximity.py +++ b/homeassistant/components/proximity.py @@ -49,9 +49,7 @@ ZONE_SCHEMA = vol.Schema({ }) CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - cv.slug: ZONE_SCHEMA, - }), + DOMAIN: cv.schema_with_slug_keys(ZONE_SCHEMA), }, extra=vol.ALLOW_EXTRA) diff --git a/homeassistant/components/remote/xiaomi_miio.py b/homeassistant/components/remote/xiaomi_miio.py index a247cb3e914..c8ffd043321 100644 --- a/homeassistant/components/remote/xiaomi_miio.py +++ b/homeassistant/components/remote/xiaomi_miio.py @@ -57,7 +57,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(ATTR_HIDDEN, default=True): cv.boolean, vol.Required(CONF_TOKEN): vol.All(str, vol.Length(min=32, max=32)), vol.Optional(CONF_COMMANDS, default={}): - vol.Schema({cv.slug: COMMAND_SCHEMA}), + cv.schema_with_slug_keys(COMMAND_SCHEMA), }, extra=vol.ALLOW_EXTRA) diff --git a/homeassistant/components/rest_command.py b/homeassistant/components/rest_command.py index 3f9b258634d..c8fb1568152 100644 --- a/homeassistant/components/rest_command.py +++ b/homeassistant/components/rest_command.py @@ -47,9 +47,7 @@ COMMAND_SCHEMA = vol.Schema({ }) CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - cv.slug: COMMAND_SCHEMA, - }), + DOMAIN: cv.schema_with_slug_keys(COMMAND_SCHEMA), }, extra=vol.ALLOW_EXTRA) diff --git a/homeassistant/components/sensor/lacrosse.py b/homeassistant/components/sensor/lacrosse.py index a2dbaa8f324..32b1dac9250 100644 --- a/homeassistant/components/sensor/lacrosse.py +++ b/homeassistant/components/sensor/lacrosse.py @@ -45,7 +45,7 @@ SENSOR_SCHEMA = vol.Schema({ }) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_SENSORS): vol.Schema({cv.slug: SENSOR_SCHEMA}), + vol.Required(CONF_SENSORS): cv.schema_with_slug_keys(SENSOR_SCHEMA), vol.Optional(CONF_BAUD, default=DEFAULT_BAUD): cv.string, vol.Optional(CONF_DATARATE): cv.positive_int, vol.Optional(CONF_DEVICE, default=DEFAULT_DEVICE): cv.string, diff --git a/homeassistant/components/sensor/sma.py b/homeassistant/components/sensor/sma.py index 4bfa62bf6dd..61009a472fb 100644 --- a/homeassistant/components/sensor/sma.py +++ b/homeassistant/components/sensor/sma.py @@ -68,9 +68,9 @@ PLATFORM_SCHEMA = vol.All(PLATFORM_SCHEMA.extend({ vol.Required(CONF_PASSWORD): cv.string, vol.Optional(CONF_GROUP, default=GROUPS[0]): vol.In(GROUPS), vol.Optional(CONF_SENSORS, default={}): - vol.Schema({cv.slug: cv.ensure_list}), + cv.schema_with_slug_keys(cv.ensure_list), vol.Optional(CONF_CUSTOM, default={}): - vol.Schema({cv.slug: CUSTOM_SCHEMA}), + cv.schema_with_slug_keys(CUSTOM_SCHEMA), }, extra=vol.PREVENT_EXTRA), _check_sensor_schema) diff --git a/homeassistant/components/sensor/template.py b/homeassistant/components/sensor/template.py index 3fa45935617..5f3af4a06a4 100644 --- a/homeassistant/components/sensor/template.py +++ b/homeassistant/components/sensor/template.py @@ -36,7 +36,7 @@ SENSOR_SCHEMA = vol.Schema({ }) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_SENSORS): vol.Schema({cv.slug: SENSOR_SCHEMA}), + vol.Required(CONF_SENSORS): cv.schema_with_slug_keys(SENSOR_SCHEMA), }) diff --git a/homeassistant/components/shell_command.py b/homeassistant/components/shell_command.py index 2a95dd5c144..f9ec8da54e3 100644 --- a/homeassistant/components/shell_command.py +++ b/homeassistant/components/shell_command.py @@ -21,9 +21,7 @@ DOMAIN = 'shell_command' _LOGGER = logging.getLogger(__name__) CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - cv.slug: cv.string, - }), + DOMAIN: cv.schema_with_slug_keys(cv.string), }, extra=vol.ALLOW_EXTRA) diff --git a/homeassistant/components/switch/broadlink.py b/homeassistant/components/switch/broadlink.py index 685402611a0..9c17767f033 100644 --- a/homeassistant/components/switch/broadlink.py +++ b/homeassistant/components/switch/broadlink.py @@ -59,7 +59,7 @@ MP1_SWITCH_SLOT_SCHEMA = vol.Schema({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_SWITCHES, default={}): - vol.Schema({cv.slug: SWITCH_SCHEMA}), + cv.schema_with_slug_keys(SWITCH_SCHEMA), vol.Optional(CONF_SLOTS, default={}): MP1_SWITCH_SLOT_SCHEMA, vol.Required(CONF_HOST): cv.string, vol.Required(CONF_MAC): cv.string, diff --git a/homeassistant/components/switch/command_line.py b/homeassistant/components/switch/command_line.py index d25c5708316..4edbd79ee0c 100644 --- a/homeassistant/components/switch/command_line.py +++ b/homeassistant/components/switch/command_line.py @@ -28,7 +28,7 @@ SWITCH_SCHEMA = vol.Schema({ }) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_SWITCHES): vol.Schema({cv.slug: SWITCH_SCHEMA}), + vol.Required(CONF_SWITCHES): cv.schema_with_slug_keys(SWITCH_SCHEMA), }) diff --git a/homeassistant/components/switch/kankun.py b/homeassistant/components/switch/kankun.py index 59966739b91..86e7fcdab3e 100644 --- a/homeassistant/components/switch/kankun.py +++ b/homeassistant/components/switch/kankun.py @@ -30,7 +30,7 @@ SWITCH_SCHEMA = vol.Schema({ }) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_SWITCHES): vol.Schema({cv.slug: SWITCH_SCHEMA}), + vol.Required(CONF_SWITCHES): cv.schema_with_slug_keys(SWITCH_SCHEMA), }) diff --git a/homeassistant/components/switch/scsgate.py b/homeassistant/components/switch/scsgate.py index bb8c067ebd9..9344aeab7ed 100644 --- a/homeassistant/components/switch/scsgate.py +++ b/homeassistant/components/switch/scsgate.py @@ -24,7 +24,8 @@ CONF_SCENARIO = 'scenario' CONF_SCS_ID = 'scs_id' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_DEVICES): vol.Schema({cv.slug: scsgate.SCSGATE_SCHEMA}), + vol.Required(CONF_DEVICES): + cv.schema_with_slug_keys(scsgate.SCSGATE_SCHEMA), }) diff --git a/homeassistant/components/switch/telnet.py b/homeassistant/components/switch/telnet.py index 440279a70a8..7c3baf2981a 100644 --- a/homeassistant/components/switch/telnet.py +++ b/homeassistant/components/switch/telnet.py @@ -32,7 +32,7 @@ SWITCH_SCHEMA = vol.Schema({ }) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_SWITCHES): vol.Schema({cv.slug: SWITCH_SCHEMA}), + vol.Required(CONF_SWITCHES): cv.schema_with_slug_keys(SWITCH_SCHEMA), }) SCAN_INTERVAL = timedelta(seconds=10) diff --git a/homeassistant/components/switch/template.py b/homeassistant/components/switch/template.py index 51cea68f6b3..a2098c2f5fd 100644 --- a/homeassistant/components/switch/template.py +++ b/homeassistant/components/switch/template.py @@ -38,7 +38,7 @@ SWITCH_SCHEMA = vol.Schema({ }) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_SWITCHES): vol.Schema({cv.slug: SWITCH_SCHEMA}), + vol.Required(CONF_SWITCHES): cv.schema_with_slug_keys(SWITCH_SCHEMA), }) diff --git a/homeassistant/components/timer/__init__.py b/homeassistant/components/timer/__init__.py index 364975671c4..b898c577bb2 100644 --- a/homeassistant/components/timer/__init__.py +++ b/homeassistant/components/timer/__init__.py @@ -53,14 +53,14 @@ SERVICE_SCHEMA_DURATION = vol.Schema({ }) CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - cv.slug: vol.Any({ + DOMAIN: cv.schema_with_slug_keys( + vol.Any({ vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_ICON): cv.icon, vol.Optional(CONF_DURATION, timedelta(DEFAULT_DURATION)): cv.time_period, }, None) - }) + ) }, extra=vol.ALLOW_EXTRA) diff --git a/homeassistant/components/volvooncall.py b/homeassistant/components/volvooncall.py index 9f9b58ec8b6..ce4dccbaf75 100644 --- a/homeassistant/components/volvooncall.py +++ b/homeassistant/components/volvooncall.py @@ -93,8 +93,8 @@ CONFIG_SCHEMA = vol.Schema({ vol.Required(CONF_PASSWORD): cv.string, vol.Optional(CONF_UPDATE_INTERVAL, default=DEFAULT_UPDATE_INTERVAL): ( vol.All(cv.time_period, vol.Clamp(min=MIN_UPDATE_INTERVAL))), - vol.Optional(CONF_NAME, default={}): vol.Schema( - {cv.slug: cv.string}), + vol.Optional(CONF_NAME, default={}): + cv.schema_with_slug_keys(cv.string), vol.Optional(CONF_RESOURCES): vol.All( cv.ensure_list, [vol.In(RESOURCES)]), vol.Optional(CONF_REGION): cv.string, diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index ef0166bc16d..475135b4cce 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -329,6 +329,9 @@ def schema_with_slug_keys(value_schema: Union[T, Callable]) -> Callable: def verify(value: Dict) -> Dict: """Validate all keys are slugs and then the value_schema.""" + if not isinstance(value, dict): + raise vol.Invalid('expected dictionary') + for key in value.keys(): slug(key) return schema(value) From 0ceace96e77cf3a88985718553ea75f8fd95910a Mon Sep 17 00:00:00 2001 From: Fabien Piuzzi Date: Tue, 22 Jan 2019 06:21:59 +0100 Subject: [PATCH 229/235] Bugfix: prevent error notification when octoprint server auto detected but no configuration present. (#20303) --- homeassistant/components/octoprint.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/octoprint.py b/homeassistant/components/octoprint.py index b626e9a93b5..853ee67db9d 100644 --- a/homeassistant/components/octoprint.py +++ b/homeassistant/components/octoprint.py @@ -92,6 +92,10 @@ def setup(hass, config): discovery.listen(hass, SERVICE_OCTOPRINT, device_discovered) + if DOMAIN not in config: + # Skip the setup if there is no configuration present + return True + for printer in config[DOMAIN]: name = printer[CONF_NAME] ssl = 's' if printer[CONF_SSL] else '' From 6d0ac30687407b8884f72722bed4d272a66cca21 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 21 Jan 2019 21:23:00 -0800 Subject: [PATCH 230/235] Bumped version to 0.86.0b3 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 36d66939e78..0191c09551b 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 86 -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 b7218e6a1d213f41493bfdf46119609b85d46643 Mon Sep 17 00:00:00 2001 From: Richard Mitchell Date: Tue, 22 Jan 2019 17:50:21 +0000 Subject: [PATCH 231/235] Should require the 'GATTOOL' setup extras which includes pexpect. (#20263) * Should require the 'GATTOOL' setup extras which includes pexpect. * Also fix skybeacon's requirement and requirements_all. --- homeassistant/components/device_tracker/bluetooth_le_tracker.py | 2 +- homeassistant/components/sensor/skybeacon.py | 2 +- requirements_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/device_tracker/bluetooth_le_tracker.py b/homeassistant/components/device_tracker/bluetooth_le_tracker.py index a07fdfdcf81..825ef04ccc5 100644 --- a/homeassistant/components/device_tracker/bluetooth_le_tracker.py +++ b/homeassistant/components/device_tracker/bluetooth_le_tracker.py @@ -15,7 +15,7 @@ import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['pygatt==3.2.0'] +REQUIREMENTS = ['pygatt[GATTTOOL]==3.2.0'] BLE_PREFIX = 'BLE_' MIN_SEEN_NEW = 5 diff --git a/homeassistant/components/sensor/skybeacon.py b/homeassistant/components/sensor/skybeacon.py index 441053a7e7e..6960999306d 100644 --- a/homeassistant/components/sensor/skybeacon.py +++ b/homeassistant/components/sensor/skybeacon.py @@ -16,7 +16,7 @@ from homeassistant.const import ( import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['pygatt==3.2.0'] +REQUIREMENTS = ['pygatt[GATTTOOL]==3.2.0'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 7a3cca7a7ee..20b773d0a59 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1003,7 +1003,7 @@ pyfttt==0.3 # homeassistant.components.device_tracker.bluetooth_le_tracker # homeassistant.components.sensor.skybeacon -pygatt==3.2.0 +pygatt[GATTTOOL]==3.2.0 # homeassistant.components.cover.gogogate2 pygogogate2==0.1.1 From 4b7d944a74cb206fd138a068ddab2c035be09407 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Wed, 23 Jan 2019 05:04:13 +0100 Subject: [PATCH 232/235] Fix xiaomi speed attribute name clash (#20312) --- homeassistant/components/fan/xiaomi_miio.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/fan/xiaomi_miio.py b/homeassistant/components/fan/xiaomi_miio.py index d3b52622a95..e0d51279bbf 100644 --- a/homeassistant/components/fan/xiaomi_miio.py +++ b/homeassistant/components/fan/xiaomi_miio.py @@ -111,7 +111,7 @@ ATTR_TRANS_LEVEL = 'trans_level' ATTR_HARDWARE_VERSION = 'hardware_version' # Air Humidifier CA -ATTR_SPEED = 'speed' +ATTR_MOTOR_SPEED = 'motor_speed' ATTR_DEPTH = 'depth' ATTR_DRY = 'dry' @@ -223,7 +223,7 @@ AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER = { AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER_CA = { **AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER_COMMON, - ATTR_SPEED: 'speed', + ATTR_MOTOR_SPEED: 'speed', ATTR_DEPTH: 'depth', ATTR_DRY: 'dry', } From 80aa2075c629eb2bcac73d780bf9549e02ff97f3 Mon Sep 17 00:00:00 2001 From: rolfberkenbosch <30292281+rolfberkenbosch@users.noreply.github.com> Date: Tue, 22 Jan 2019 17:24:40 +0100 Subject: [PATCH 233/235] Update locationsharinglib to version 3.0.11 (#20322) * Update google_maps.py There are known bug in locationsharinglib version 3.0.9, the developer has fixed this in 3.0.11. (https://github.com/costastf/locationsharinglib/issues/42) * Update requirements_all.txt --- 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 1f95414541c..c324f3c2757 100644 --- a/homeassistant/components/device_tracker/google_maps.py +++ b/homeassistant/components/device_tracker/google_maps.py @@ -19,7 +19,7 @@ from homeassistant.helpers.event import track_time_interval from homeassistant.helpers.typing import ConfigType from homeassistant.util import slugify, dt as dt_util -REQUIREMENTS = ['locationsharinglib==3.0.9'] +REQUIREMENTS = ['locationsharinglib==3.0.11'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 20b773d0a59..4f96d15c61f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -625,7 +625,7 @@ liveboxplaytv==2.0.2 lmnotify==0.0.4 # homeassistant.components.device_tracker.google_maps -locationsharinglib==3.0.9 +locationsharinglib==3.0.11 # homeassistant.components.logi_circle logi_circle==0.1.7 From 4662ab215cb20d3ee02d2fa4efac6781d2b01ec7 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 22 Jan 2019 14:07:17 -0800 Subject: [PATCH 234/235] Fix invalid entity ID in entity registry (#20328) --- homeassistant/helpers/entity_registry.py | 11 ++++++-- tests/helpers/test_entity_registry.py | 34 ++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index 8216681496b..82530708838 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -122,8 +122,15 @@ class EntityRegistry: entity_id = self.async_get_entity_id(domain, platform, unique_id) if entity_id: return self._async_update_entity( - entity_id, config_entry_id=config_entry_id, - device_id=device_id) + entity_id, + config_entry_id=config_entry_id, + device_id=device_id, + # When we changed our slugify algorithm, we invalidated some + # stored entity IDs with either a __ or ending in _. + # Fix introduced in 0.86 (Jan 23, 2018). Next line can be + # removed when we release 1.0 or in 2019. + new_entity_id='.'.join(slugify(part) for part + in entity_id.split('.', 1))) entity_id = self.async_generate_entity_id( domain, suggested_object_id or '{}_{}'.format(platform, unique_id), diff --git a/tests/helpers/test_entity_registry.py b/tests/helpers/test_entity_registry.py index a8c9086b2d2..ef7b4a60ee2 100644 --- a/tests/helpers/test_entity_registry.py +++ b/tests/helpers/test_entity_registry.py @@ -4,6 +4,7 @@ from unittest.mock import patch import pytest +from homeassistant.core import valid_entity_id from homeassistant.helpers import entity_registry from tests.common import mock_registry, flush_store @@ -222,3 +223,36 @@ async def test_migration(hass): assert entry.name == 'Test Name' assert entry.disabled_by == 'hass' assert entry.config_entry_id == 'test-config-id' + + +async def test_loading_invalid_entity_id(hass, hass_storage): + """Test we autofix invalid entity IDs.""" + hass_storage[entity_registry.STORAGE_KEY] = { + 'version': entity_registry.STORAGE_VERSION, + 'data': { + 'entities': [ + { + 'entity_id': 'test.invalid__middle', + 'platform': 'super_platform', + 'unique_id': 'id-invalid-middle', + 'name': 'registry override', + }, { + 'entity_id': 'test.invalid_end_', + 'platform': 'super_platform', + 'unique_id': 'id-invalid-end', + } + ] + } + } + + registry = await entity_registry.async_get_registry(hass) + + entity_invalid_middle = registry.async_get_or_create( + 'test', 'super_platform', 'id-invalid-middle') + + assert valid_entity_id(entity_invalid_middle.entity_id) + + entity_invalid_end = registry.async_get_or_create( + 'test', 'super_platform', 'id-invalid-end') + + assert valid_entity_id(entity_invalid_end.entity_id) From cdcc535ae199280c61360bea3ed9dd2e816d1e5d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 23 Jan 2019 10:48:55 -0800 Subject: [PATCH 235/235] Version bump to 0.86.0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 0191c09551b..ef9f7b38384 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 86 -PATCH_VERSION = '0b3' +PATCH_VERSION = '0' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3)