From 80f98b9ea21646e991ce50851af53cb8a0e2ddd4 Mon Sep 17 00:00:00 2001 From: Steven Looman Date: Thu, 16 Aug 2018 22:14:13 +0200 Subject: [PATCH 001/247] Fix message "Updating dlna_dmr media_player took longer than ..." --- homeassistant/components/media_player/dlna_dmr.py | 4 ++-- requirements_all.txt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/media_player/dlna_dmr.py b/homeassistant/components/media_player/dlna_dmr.py index 9b6beb83341..c40e3ed0ca9 100644 --- a/homeassistant/components/media_player/dlna_dmr.py +++ b/homeassistant/components/media_player/dlna_dmr.py @@ -35,7 +35,7 @@ from homeassistant.util import get_local_ip DLNA_DMR_DATA = 'dlna_dmr' REQUIREMENTS = [ - 'async-upnp-client==0.12.2', + 'async-upnp-client==0.12.3', ] DEFAULT_NAME = 'DLNA Digital Media Renderer' @@ -126,7 +126,7 @@ async def async_setup_platform(hass: HomeAssistant, name = config.get(CONF_NAME) elif discovery_info is not None: url = discovery_info['ssdp_description'] - name = discovery_info['name'] + name = discovery_info.get('name') if DLNA_DMR_DATA not in hass.data: hass.data[DLNA_DMR_DATA] = {} diff --git a/requirements_all.txt b/requirements_all.txt index 894e9ef1706..ed12752a934 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -138,7 +138,7 @@ apns2==0.3.0 asterisk_mbox==0.4.0 # homeassistant.components.media_player.dlna_dmr -async-upnp-client==0.12.2 +async-upnp-client==0.12.3 # homeassistant.components.light.avion # avion==0.7 From 71d00062e594e0305de259885227509349c33495 Mon Sep 17 00:00:00 2001 From: Steven Looman Date: Fri, 17 Aug 2018 21:25:08 +0200 Subject: [PATCH 002/247] Upgrade to async_upnp_client 0.12.4 --- homeassistant/components/media_player/dlna_dmr.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/media_player/dlna_dmr.py b/homeassistant/components/media_player/dlna_dmr.py index c40e3ed0ca9..ebb1ab8d383 100644 --- a/homeassistant/components/media_player/dlna_dmr.py +++ b/homeassistant/components/media_player/dlna_dmr.py @@ -35,7 +35,7 @@ from homeassistant.util import get_local_ip DLNA_DMR_DATA = 'dlna_dmr' REQUIREMENTS = [ - 'async-upnp-client==0.12.3', + 'async-upnp-client==0.12.4', ] DEFAULT_NAME = 'DLNA Digital Media Renderer' diff --git a/requirements_all.txt b/requirements_all.txt index ed12752a934..9f0fe83b5ac 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -138,7 +138,7 @@ apns2==0.3.0 asterisk_mbox==0.4.0 # homeassistant.components.media_player.dlna_dmr -async-upnp-client==0.12.3 +async-upnp-client==0.12.4 # homeassistant.components.light.avion # avion==0.7 From 839b58c6001fd026af9c4b947e9dd3259d70e64a Mon Sep 17 00:00:00 2001 From: Steven Looman Date: Fri, 17 Aug 2018 21:28:29 +0200 Subject: [PATCH 003/247] Working on IGD --- homeassistant/components/discovery.py | 1 + homeassistant/components/igd/__init__.py | 187 ++++++++++++++++++ homeassistant/components/igd/config_flow.py | 103 ++++++++++ homeassistant/components/igd/const.py | 5 + homeassistant/components/igd/strings.json | 26 +++ .../components/sensor/{upnp.py => igd.py} | 21 +- homeassistant/components/upnp.py | 140 ------------- homeassistant/config_entries.py | 2 + .../components/{test_upnp.py => test_igd.py} | 10 +- 9 files changed, 339 insertions(+), 156 deletions(-) create mode 100644 homeassistant/components/igd/__init__.py create mode 100644 homeassistant/components/igd/config_flow.py create mode 100644 homeassistant/components/igd/const.py create mode 100644 homeassistant/components/igd/strings.json rename homeassistant/components/sensor/{upnp.py => igd.py} (77%) delete mode 100644 homeassistant/components/upnp.py rename tests/components/{test_upnp.py => test_igd.py} (94%) diff --git a/homeassistant/components/discovery.py b/homeassistant/components/discovery.py index b400d1d8885..d686b114095 100644 --- a/homeassistant/components/discovery.py +++ b/homeassistant/components/discovery.py @@ -49,6 +49,7 @@ CONFIG_ENTRY_HANDLERS = { 'google_cast': 'cast', SERVICE_HUE: 'hue', 'sonos': 'sonos', + 'igd': 'igd', } SERVICE_HANDLERS = { diff --git a/homeassistant/components/igd/__init__.py b/homeassistant/components/igd/__init__.py new file mode 100644 index 00000000000..18aa10e391d --- /dev/null +++ b/homeassistant/components/igd/__init__.py @@ -0,0 +1,187 @@ +""" +Will open a port in your router for Home Assistant and provide statistics. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/upnp/ +""" +from ipaddress import ip_address +import aiohttp +import asyncio + +import voluptuous as vol + +from homeassistant import config_entries, data_entry_flow +from homeassistant.const import ( + CONF_URL, +) +from homeassistant.exceptions import PlatformNotReady +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.util import get_local_ip + +from .config_flow import configured_hosts +from .const import DOMAIN +from .const import LOGGER as _LOGGER + +_LOGGER.warning('Loading IGD') + + +REQUIREMENTS = ['async-upnp-client==0.12.3'] +DEPENDENCIES = ['http', 'api'] + +CONF_LOCAL_IP = 'local_ip' +CONF_ENABLE_PORT_MAPPING = 'port_mapping' +CONF_PORTS = 'ports' +CONF_UNITS = 'unit' +CONF_HASS = 'hass' + +NOTIFICATION_ID = 'igd_notification' +NOTIFICATION_TITLE = 'UPnP/IGD Setup' + +IP_SERVICE = 'urn:schemas-upnp-org:service:WANIPConnection:1' # XXX TODO: remove this + +UNITS = { + "Bytes": 1, + "KBytes": 1024, + "MBytes": 1024**2, + "GBytes": 1024**3, +} + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_URL): cv.url, + vol.Optional(CONF_ENABLE_PORT_MAPPING, default=True): cv.boolean, + vol.Optional(CONF_UNITS, default="MBytes"): vol.In(UNITS), + vol.Optional(CONF_LOCAL_IP): vol.All(ip_address, cv.string), + vol.Optional(CONF_PORTS): + vol.Schema({vol.Any(CONF_HASS, cv.positive_int): cv.positive_int}) + }), +}, extra=vol.ALLOW_EXTRA) + + +async def async_setup(hass, config, *args, **kwargs): + """Register a port mapping for Home Assistant via UPnP.""" + conf = config.get(DOMAIN) + if conf is None: + conf = {} + + hass.data[DOMAIN] = {} + configured = configured_hosts(hass) + _LOGGER.debug('Config: %s', config) + _LOGGER.debug('configured: %s', configured) + + igds = [] + if not igds: + return True + + for igd_conf in igds: + hass.async_add_job(hass.config_entries.flow.async_init( + DOMAIN, context={'source': config_entries.SOURCE_IMPORT}, + data={ + 'ssdp_url': igd_conf['ssdp_url'], + } + )) + + return True + + # if host is None: + # host = get_local_ip() + # + # if host == '127.0.0.1': + # _LOGGER.error( + # 'Unable to determine local IP. Add it to your configuration.') + # return False + # + # url = config.get(CONF_URL) + # + # # build requester + # from async_upnp_client.aiohttp import AiohttpSessionRequester + # session = async_get_clientsession(hass) + # requester = AiohttpSessionRequester(session, True) + # + # # create upnp device + # from async_upnp_client import UpnpFactory + # factory = UpnpFactory(requester, disable_state_variable_validation=True) + # try: + # upnp_device = await factory.async_create_device(url) + # except (asyncio.TimeoutError, aiohttp.ClientError): + # raise PlatformNotReady() + # + # # wrap with IgdDevice + # from async_upnp_client.igd import IgdDevice + # igd_device = IgdDevice(upnp_device, None) + # hass.data[DATA_IGD]['device'] = igd_device + # + # # sensors + # unit = config.get(CONF_UNITS) + # hass.async_create_task(discovery.async_load_platform( + # hass, 'sensor', DOMAIN, {'unit': unit}, config)) + # + # # port mapping + # port_mapping = config.get(CONF_ENABLE_PORT_MAPPING) + # if not port_mapping: + # return True + # + # # determine ports + # internal_port = hass.http.server_port + # ports = config.get(CONF_PORTS) + # if ports is None: + # ports = {CONF_HASS: internal_port} + # + # registered = [] + # async def register_port_mappings(event): + # """(Re-)register the port mapping.""" + # from async_upnp_client import UpnpError + # for internal, external in ports.items(): + # if internal == CONF_HASS: + # internal = internal_port + # try: + # await igd_device.async_add_port_mapping(remote_host=None, + # external_port=external, + # protocol='TCP', + # internal_port=internal, + # internal_client=ip_address(host), + # enabled=True, + # description='Home Assistant', + # lease_duration=None) + # registered.append(external) + # _LOGGER.debug("external %s -> %s @ %s", external, internal, host) + # except UpnpError as error: + # _LOGGER.error(error) + # hass.components.persistent_notification.create( + # 'ERROR: TCP port {} is already mapped in your router.' + # '
Please disable port_mapping in the upnp ' + # 'configuration section.
' + # 'You will need to restart hass after fixing.' + # ''.format(external), + # title=NOTIFICATION_TITLE, + # notification_id=NOTIFICATION_ID) + # + # async def deregister_port_mappings(event): + # """De-register the port mapping.""" + # tasks = [igd_device.async_delete_port_mapping(remote_host=None, + # external_port=external, + # protocol='TCP') + # for external in registered] + # if tasks: + # await asyncio.wait(tasks) + # + # await register_port_mappings(None) + # hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, deregister_port_mappings) + # + # return True + + +async def async_setup_entry(hass, entry): + """Set up a bridge from a config entry.""" + _LOGGER.debug('async_setup_entry, title: %s, data: %s', entry.title, entry.data) + + # port mapping? + # sensors + + return True + +async def async_unload_entry(hass, entry): + """Unload a config entry.""" + + diff --git a/homeassistant/components/igd/config_flow.py b/homeassistant/components/igd/config_flow.py new file mode 100644 index 00000000000..2eb1aae5b80 --- /dev/null +++ b/homeassistant/components/igd/config_flow.py @@ -0,0 +1,103 @@ +"""Config flow for IGD.""" +from homeassistant import config_entries, data_entry_flow +from homeassistant.core import callback +from homeassistant.helpers import config_validation as cv + +import voluptuous as vol + +from .const import DOMAIN +from .const import LOGGER as _LOGGER + + +@callback +def configured_hosts(hass): + """Return a set of the configured hosts.""" + return set(entry.data['ssdp_url'] + for entry in hass.config_entries.async_entries(DOMAIN)) + + +async def _get_igd_device(hass, ssdp_url): + """.""" + # build requester + from async_upnp_client.aiohttp import AiohttpSessionRequester + session = async_get_clientsession(hass) + requester = AiohttpSessionRequester(session, True) + + # create upnp device + from async_upnp_client import UpnpFactory + factory = UpnpFactory(requester, disable_state_variable_validation=True) + try: + upnp_device = await factory.async_create_device(ssdp_url) + except (asyncio.TimeoutError, aiohttp.ClientError): + raise PlatformNotReady() + + # wrap with IgdDevice + from async_upnp_client.igd import IgdDevice + igd_device = IgdDevice(upnp_device, None) + return igd_device + + +@config_entries.HANDLERS.register(DOMAIN) +class IgdFlowHandler(data_entry_flow.FlowHandler): + """Handle a Hue config flow.""" + + VERSION = 1 + + # def __init__(self): + # """Initializer.""" + # self.host = None + + # flow: 1. detection/user adding + # 2. question: port forward? sensors? + # 3. add it! + + async def async_step_user(self, user_input=None): + _LOGGER.debug('async_step_user: %s', user_input) + return await self.async_abort(reason='todo') + + async def async_step_discovery(self, discovery_info): + """Handle a discovered IGD. + + This flow is triggered by the discovery component. It will check if the + host is already configured and delegate to the import step if not. + """ + _LOGGER.debug('async_step_discovery: %s', discovery_info) + + ssdp_url = discovery_info['ssdp_description'] + return await self.async_step_options({ + 'ssdp_url': ssdp_url, + }) + + async def async_step_options(self, user_options): + """.""" + _LOGGER.debug('async_step_options: %s', user_options) + if user_options and \ + 'sensors' in user_options and \ + 'port_forward' in user_options: + return await self.async_step_import(user_options) + + return self.async_show_form( + step_id='options', + data_schema=vol.Schema({ + vol.Required('sensors'): cv.boolean, + vol.Required('port_forward'): cv.boolean, + # vol.Optional('ssdp_url', default=user_options['ssdp_url']): cv.url, + }) + ) + + async def async_step_import(self, import_info): + """Import a IGD as new entry.""" + _LOGGER.debug('async_step_import: %s', import_info) + + ssdp_url = import_info['ssdp_url'] + try: + igd_device = await _get_igd_device(self.hass, ssdp_url) # try it to see if it works + except: + pass + return self.async_create_entry( + title=igd_device.name, + data={ + 'ssdp_url': ssdp_url, + 'udn': igd_device.udn, + } + ) \ No newline at end of file diff --git a/homeassistant/components/igd/const.py b/homeassistant/components/igd/const.py new file mode 100644 index 00000000000..a933497bd3e --- /dev/null +++ b/homeassistant/components/igd/const.py @@ -0,0 +1,5 @@ +"""Constants for the IGD component.""" +import logging + +DOMAIN = 'igd' +LOGGER = logging.getLogger('homeassistant.components.igd') diff --git a/homeassistant/components/igd/strings.json b/homeassistant/components/igd/strings.json new file mode 100644 index 00000000000..b88ed5c7968 --- /dev/null +++ b/homeassistant/components/igd/strings.json @@ -0,0 +1,26 @@ +{ + "config": { + "title": "IGD", + "step": { + "options": { + "title": "Extra configuration options for the IGD", + "data":{ + "sensors": "Add traffic in/out sensors", + "port_forward": "Enable port forward for Home Assistant\nOnly enable this when your Home Assistant is password protected!", + "ssdp_ur": "SSDP URL", + } + }, + "import": { + "title": "Link with IGD", + "description": "Setup the IGD" + } + }, + "error": { + }, + "abort": { + "already_configured": "IGD is already configured", + "no_igds": "No IGDs discovered", + "todo": "TODO" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/upnp.py b/homeassistant/components/sensor/igd.py similarity index 77% rename from homeassistant/components/sensor/upnp.py rename to homeassistant/components/sensor/igd.py index 07b63553fcb..53692a3432f 100644 --- a/homeassistant/components/sensor/upnp.py +++ b/homeassistant/components/sensor/igd.py @@ -6,12 +6,12 @@ https://home-assistant.io/components/sensor.upnp/ """ import logging -from homeassistant.components.upnp import DATA_UPNP, UNITS, CIC_SERVICE +from homeassistant.components.igd import DATA_IGD, UNITS from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) -DEPENDENCIES = ['upnp'] +DEPENDENCIES = ['igd'] BYTES_RECEIVED = 1 BYTES_SENT = 2 @@ -33,20 +33,19 @@ async def async_setup_platform(hass, config, async_add_devices, if discovery_info is None: return - device = hass.data[DATA_UPNP] - service = device.find_first_service(CIC_SERVICE) + device = hass.data[DATA_IGD]['device'] unit = discovery_info['unit'] async_add_devices([ - IGDSensor(service, t, unit if SENSOR_TYPES[t][1] else '#') + IGDSensor(device, t, unit if SENSOR_TYPES[t][1] else '#') for t in SENSOR_TYPES], True) class IGDSensor(Entity): """Representation of a UPnP IGD sensor.""" - def __init__(self, service, sensor_type, unit=None): + def __init__(self, device, sensor_type, unit=None): """Initialize the IGD sensor.""" - self._service = service + self._device = device self.type = sensor_type self.unit = unit self.unit_factor = UNITS[unit] if unit in UNITS else 1 @@ -78,10 +77,10 @@ class IGDSensor(Entity): async def async_update(self): """Get the latest information from the IGD.""" if self.type == BYTES_RECEIVED: - self._state = await self._service.get_total_bytes_received() + self._state = await self._device.async_get_total_bytes_received() elif self.type == BYTES_SENT: - self._state = await self._service.get_total_bytes_sent() + self._state = await self._device.async_get_total_bytes_sent() elif self.type == PACKETS_RECEIVED: - self._state = await self._service.get_total_packets_received() + self._state = await self._device.async_get_total_packets_received() elif self.type == PACKETS_SENT: - self._state = await self._service.get_total_packets_sent() + self._state = await self._device.async_get_total_packets_sent() diff --git a/homeassistant/components/upnp.py b/homeassistant/components/upnp.py deleted file mode 100644 index b4fe9d3fce9..00000000000 --- a/homeassistant/components/upnp.py +++ /dev/null @@ -1,140 +0,0 @@ -""" -Will open a port in your router for Home Assistant and provide statistics. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/upnp/ -""" -from ipaddress import ip_address -import logging -import asyncio - -import voluptuous as vol - -from homeassistant.const import (EVENT_HOMEASSISTANT_STOP) -from homeassistant.helpers import config_validation as cv -from homeassistant.helpers import discovery -from homeassistant.util import get_local_ip - -REQUIREMENTS = ['pyupnp-async==0.1.0.2'] -DEPENDENCIES = ['http'] - -_LOGGER = logging.getLogger(__name__) - -DEPENDENCIES = ['api'] -DOMAIN = 'upnp' - -DATA_UPNP = 'upnp_device' - -CONF_LOCAL_IP = 'local_ip' -CONF_ENABLE_PORT_MAPPING = 'port_mapping' -CONF_PORTS = 'ports' -CONF_UNITS = 'unit' -CONF_HASS = 'hass' - -NOTIFICATION_ID = 'upnp_notification' -NOTIFICATION_TITLE = 'UPnP Setup' - -IGD_DEVICE = 'urn:schemas-upnp-org:device:InternetGatewayDevice:1' -PPP_SERVICE = 'urn:schemas-upnp-org:service:WANPPPConnection:1' -IP_SERVICE = 'urn:schemas-upnp-org:service:WANIPConnection:1' -CIC_SERVICE = 'urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1' - -UNITS = { - "Bytes": 1, - "KBytes": 1024, - "MBytes": 1024**2, - "GBytes": 1024**3, -} - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Optional(CONF_ENABLE_PORT_MAPPING, default=True): cv.boolean, - vol.Optional(CONF_UNITS, default="MBytes"): vol.In(UNITS), - vol.Optional(CONF_LOCAL_IP): vol.All(ip_address, cv.string), - vol.Optional(CONF_PORTS): - vol.Schema({vol.Any(CONF_HASS, cv.positive_int): cv.positive_int}) - }), -}, extra=vol.ALLOW_EXTRA) - - -async def async_setup(hass, config): - """Register a port mapping for Home Assistant via UPnP.""" - config = config[DOMAIN] - host = config.get(CONF_LOCAL_IP) - - if host is None: - host = get_local_ip() - - if host == '127.0.0.1': - _LOGGER.error( - 'Unable to determine local IP. Add it to your configuration.') - return False - - import pyupnp_async - from pyupnp_async.error import UpnpSoapError - - service = None - resp = await pyupnp_async.msearch_first(search_target=IGD_DEVICE) - if not resp: - return False - - try: - device = await resp.get_device() - hass.data[DATA_UPNP] = device - for _service in device.services: - if _service['serviceType'] == PPP_SERVICE: - service = device.find_first_service(PPP_SERVICE) - if _service['serviceType'] == IP_SERVICE: - service = device.find_first_service(IP_SERVICE) - if _service['serviceType'] == CIC_SERVICE: - unit = config.get(CONF_UNITS) - hass.async_create_task(discovery.async_load_platform( - hass, 'sensor', DOMAIN, {'unit': unit}, config)) - except UpnpSoapError as error: - _LOGGER.error(error) - return False - - if not service: - _LOGGER.warning("Could not find any UPnP IGD") - return False - - port_mapping = config.get(CONF_ENABLE_PORT_MAPPING) - if not port_mapping: - return True - - internal_port = hass.http.server_port - - ports = config.get(CONF_PORTS) - if ports is None: - ports = {CONF_HASS: internal_port} - - registered = [] - for internal, external in ports.items(): - if internal == CONF_HASS: - internal = internal_port - try: - await service.add_port_mapping(internal, external, host, 'TCP', - desc='Home Assistant') - registered.append(external) - _LOGGER.debug("external %s -> %s @ %s", external, internal, host) - except UpnpSoapError as error: - _LOGGER.error(error) - hass.components.persistent_notification.create( - 'ERROR: tcp port {} is already mapped in your router.' - '
Please disable port_mapping in the upnp ' - 'configuration section.
' - 'You will need to restart hass after fixing.' - ''.format(external), - title=NOTIFICATION_TITLE, - notification_id=NOTIFICATION_ID) - - async def deregister_port(event): - """De-register the UPnP port mapping.""" - tasks = [service.delete_port_mapping(external, 'TCP') - for external in registered] - if tasks: - await asyncio.wait(tasks) - - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, deregister_port) - - return True diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index b2e8389e449..f3b04f64e05 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -142,6 +142,7 @@ FLOWS = [ 'nest', 'sonos', 'zone', + 'igd', ] @@ -414,6 +415,7 @@ class ConfigEntries: Handler key is the domain of the component that we want to setup. """ component = getattr(self.hass.components, handler_key) + _LOGGER.debug('Handler key: %s', handler_key) handler = HANDLERS.get(handler_key) if handler is None: diff --git a/tests/components/test_upnp.py b/tests/components/test_igd.py similarity index 94% rename from tests/components/test_upnp.py rename to tests/components/test_igd.py index 4956b8a6278..87f992267c6 100644 --- a/tests/components/test_upnp.py +++ b/tests/components/test_igd.py @@ -6,7 +6,7 @@ import pytest from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.setup import async_setup_component -from homeassistant.components.upnp import IP_SERVICE, DATA_UPNP +from homeassistant.components.igd import IP_SERVICE, DATA_IGD class MockService(MagicMock): @@ -107,7 +107,7 @@ async def test_setup_succeeds_if_specify_ip(hass, mock_msearch_first): }) assert result - mock_service = hass.data[DATA_UPNP].peep_first_service() + mock_service = hass.data[DATA_IGD].peep_first_service() assert len(mock_service.mock_add_port_mapping.mock_calls) == 1 mock_service.mock_add_port_mapping.assert_called_once_with( 8123, 8123, '192.168.0.10', 'TCP', desc='Home Assistant') @@ -122,7 +122,7 @@ async def test_no_config_maps_hass_local_to_remote_port(hass, }) assert result - mock_service = hass.data[DATA_UPNP].peep_first_service() + mock_service = hass.data[DATA_IGD].peep_first_service() assert len(mock_service.mock_add_port_mapping.mock_calls) == 1 mock_service.mock_add_port_mapping.assert_called_once_with( 8123, 8123, '192.168.0.10', 'TCP', desc='Home Assistant') @@ -141,7 +141,7 @@ async def test_map_hass_to_remote_port(hass, }) assert result - mock_service = hass.data[DATA_UPNP].peep_first_service() + mock_service = hass.data[DATA_IGD].peep_first_service() assert len(mock_service.mock_add_port_mapping.mock_calls) == 1 mock_service.mock_add_port_mapping.assert_called_once_with( 8123, 1000, '192.168.0.10', 'TCP', desc='Home Assistant') @@ -162,7 +162,7 @@ async def test_map_internal_to_remote_ports(hass, }) assert result - mock_service = hass.data[DATA_UPNP].peep_first_service() + mock_service = hass.data[DATA_IGD].peep_first_service() assert len(mock_service.mock_add_port_mapping.mock_calls) == 2 mock_service.mock_add_port_mapping.assert_any_call( From 1eac6408f5b708188b94984195f0494c1306b36d Mon Sep 17 00:00:00 2001 From: Steven Looman Date: Wed, 29 Aug 2018 21:19:04 +0200 Subject: [PATCH 004/247] Working on IGD --- .../components/igd/.translations/en.json | 29 ++ .../components/igd/.translations/nl.json | 29 ++ homeassistant/components/igd/__init__.py | 273 ++++++++++-------- homeassistant/components/igd/config_flow.py | 164 ++++++----- homeassistant/components/igd/const.py | 2 + homeassistant/components/igd/strings.json | 17 +- homeassistant/components/sensor/igd.py | 122 ++++++-- tests/components/igd/__init__.py | 1 + tests/components/igd/test_config_flow.py | 165 +++++++++++ tests/components/igd/test_init.py | 41 +++ tests/components/test_igd.py | 183 ------------ 11 files changed, 637 insertions(+), 389 deletions(-) create mode 100644 homeassistant/components/igd/.translations/en.json create mode 100644 homeassistant/components/igd/.translations/nl.json create mode 100644 tests/components/igd/__init__.py create mode 100644 tests/components/igd/test_config_flow.py create mode 100644 tests/components/igd/test_init.py delete mode 100644 tests/components/test_igd.py diff --git a/homeassistant/components/igd/.translations/en.json b/homeassistant/components/igd/.translations/en.json new file mode 100644 index 00000000000..bd0d2a9b7c0 --- /dev/null +++ b/homeassistant/components/igd/.translations/en.json @@ -0,0 +1,29 @@ +{ + "config": { + "title": "IGD", + "step": { + "init": { + "title": "IGD" + }, + "user": { + "title": "Configuration options for the IGD", + "data":{ + "sensors": "Add traffic in/out sensors", + "port_forward": "Enable port forward for Home Assistant\nOnly enable this when your Home Assistant is password protected!", + "ssdp_url": "SSDP URL", + "udn": "UDN", + "name": "Name" + } + } + }, + "error": { + }, + "abort": { + "no_devices_discovered": "No IGDs discovered", + "already_configured": "IGD is already configured", + "no_sensors_or_port_forward": "Enable at least sensors or Port forward", + "no_igds": "No IGDs discovered", + "todo": "TODO" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/igd/.translations/nl.json b/homeassistant/components/igd/.translations/nl.json new file mode 100644 index 00000000000..06f9122678c --- /dev/null +++ b/homeassistant/components/igd/.translations/nl.json @@ -0,0 +1,29 @@ +{ + "config": { + "title": "IGD", + "step": { + "init": { + "title": "IGD" + }, + "user": { + "title": "Extra configuratie options voor IGD", + "data":{ + "sensors": "Verkeer in/out sensors", + "port_forward": "Maak port-forward voor Home Assistant\nZet dit alleen aan wanneer uw Home Assistant een wachtwoord heeft!", + "ssdp_url": "SSDP URL", + "udn": "UDN", + "name": "Naam" + } + } + }, + "error": { + }, + "abort": { + "no_devices_discovered": "Geen IGDs gevonden", + "already_configured": "IGD is reeds geconfigureerd", + "no_sensors_or_port_forward": "Kies ten minste sensors of Port forward", + "no_igds": "Geen IGDs gevonden", + "todo": "TODO" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/igd/__init__.py b/homeassistant/components/igd/__init__.py index 18aa10e391d..b932a4f8055 100644 --- a/homeassistant/components/igd/__init__.py +++ b/homeassistant/components/igd/__init__.py @@ -4,30 +4,49 @@ Will open a port in your router for Home Assistant and provide statistics. For more details about this component, please refer to the documentation at https://home-assistant.io/components/upnp/ """ +# XXX TODO: +# + flow: +# + discovery +# + adding device +# + removing device +# - configured: +# - adding +# - sensors: +# + adding +# + handle overflow +# - removing +# - port forward: +# - adding +# - removing +# - shutdown + + +from ipaddress import IPv4Address from ipaddress import ip_address import aiohttp import asyncio import voluptuous as vol -from homeassistant import config_entries, data_entry_flow -from homeassistant.const import ( - CONF_URL, -) +from homeassistant import config_entries +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.const import CONF_URL from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers import config_validation as cv +from homeassistant.helpers import discovery from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util import get_local_ip -from .config_flow import configured_hosts +from .config_flow import configured_udns +from .const import CONF_PORT_FORWARD, CONF_SENSORS from .const import DOMAIN from .const import LOGGER as _LOGGER -_LOGGER.warning('Loading IGD') - -REQUIREMENTS = ['async-upnp-client==0.12.3'] -DEPENDENCIES = ['http', 'api'] +REQUIREMENTS = ['async-upnp-client==0.12.4'] +DEPENDENCIES = ['http'] CONF_LOCAL_IP = 'local_ip' CONF_ENABLE_PORT_MAPPING = 'port_mapping' @@ -38,8 +57,6 @@ CONF_HASS = 'hass' NOTIFICATION_ID = 'igd_notification' NOTIFICATION_TITLE = 'UPnP/IGD Setup' -IP_SERVICE = 'urn:schemas-upnp-org:service:WANIPConnection:1' # XXX TODO: remove this - UNITS = { "Bytes": 1, "KBytes": 1024, @@ -59,129 +76,157 @@ CONFIG_SCHEMA = vol.Schema({ }, extra=vol.ALLOW_EXTRA) -async def async_setup(hass, config, *args, **kwargs): - """Register a port mapping for Home Assistant via UPnP.""" - conf = config.get(DOMAIN) - if conf is None: - conf = {} +async def _async_create_igd_device(hass: HomeAssistantType, ssdp_description: str): + """.""" + # build requester + from async_upnp_client.aiohttp import AiohttpSessionRequester + session = async_get_clientsession(hass) + requester = AiohttpSessionRequester(session, True) - hass.data[DOMAIN] = {} - configured = configured_hosts(hass) - _LOGGER.debug('Config: %s', config) + # create upnp device + from async_upnp_client import UpnpFactory + factory = UpnpFactory(requester, disable_state_variable_validation=True) + try: + upnp_device = await factory.async_create_device(ssdp_description) + except (asyncio.TimeoutError, aiohttp.ClientError): + raise PlatformNotReady() + + # wrap with IgdDevice + from async_upnp_client.igd import IgdDevice + igd_device = IgdDevice(upnp_device, None) + return igd_device + + +def _store_device(hass: HomeAssistantType, udn, igd_device): + """Store an igd_device by udn.""" + hass.data[DOMAIN] = hass.data.get(DOMAIN, {}) + hass.data[DOMAIN]['devices'] = hass.data[DOMAIN].get('devices', {}) + hass.data[DOMAIN]['devices'][udn] = igd_device + + +def _get_device(hass: HomeAssistantType, udn): + """Get an igd_device by udn.""" + hass.data[DOMAIN] = hass.data.get(DOMAIN, {}) + hass.data[DOMAIN]['devices'] = hass.data[DOMAIN].get('devices', {}) + return hass.data[DOMAIN]['devices'][udn] + + +async def _async_create_port_forward(hass: HomeAssistantType, igd_device): + """Create a port forward.""" + _LOGGER.debug('Creating port forward: %s', igd_device) + + # determine local ip, ensure sane IP + local_ip = get_local_ip() + if local_ip == '127.0.0.1': + _LOGGER.warning('Could not create port forward, our IP is 127.0.0.1') + return False + local_ip = IPv4Address(local_ip) + + # create port mapping + port = hass.http.server_port + await igd_device.async_add_port_mapping(remote_host=None, + external_port=port, + protocol='TCP', + internal_port=port, + internal_client=local_ip, + enabled=True, + description="Home Assistant", + lease_duration=None) + + return True + + +async def _async_remove_port_forward(hass: HomeAssistantType, igd_device): + """Remove a port forward.""" + _LOGGER.debug('Removing port forward: %s', igd_device) + + # remove port mapping + port = hass.http.server_port + await igd_device.async_remove_port_mapping(remote_host=None, + external_port=port, + protocol='TCP') + + +# config +async def async_setup(hass: HomeAssistantType, config): + """Register a port mapping for Home Assistant via UPnP.""" + _LOGGER.debug('async_setup: config: %s', config) + conf = config.get(DOMAIN, {}) + + hass.data[DOMAIN] = hass.data.get(DOMAIN, {}) + configured = configured_udns(hass) _LOGGER.debug('configured: %s', configured) - igds = [] - if not igds: - return True + # if no ssdp given: take any discovered - by flow - IGD entry + # if none discovered, raise PlatformNotReady + # if ssdp given: use the SSDP + igds = [] # XXX TODO for igd_conf in igds: hass.async_add_job(hass.config_entries.flow.async_init( DOMAIN, context={'source': config_entries.SOURCE_IMPORT}, data={ - 'ssdp_url': igd_conf['ssdp_url'], + 'ssdp_description': igd_conf['ssdp_description'], } )) return True - # if host is None: - # host = get_local_ip() - # - # if host == '127.0.0.1': - # _LOGGER.error( - # 'Unable to determine local IP. Add it to your configuration.') - # return False - # - # url = config.get(CONF_URL) - # - # # build requester - # from async_upnp_client.aiohttp import AiohttpSessionRequester - # session = async_get_clientsession(hass) - # requester = AiohttpSessionRequester(session, True) - # - # # create upnp device - # from async_upnp_client import UpnpFactory - # factory = UpnpFactory(requester, disable_state_variable_validation=True) - # try: - # upnp_device = await factory.async_create_device(url) - # except (asyncio.TimeoutError, aiohttp.ClientError): - # raise PlatformNotReady() - # - # # wrap with IgdDevice - # from async_upnp_client.igd import IgdDevice - # igd_device = IgdDevice(upnp_device, None) - # hass.data[DATA_IGD]['device'] = igd_device - # - # # sensors - # unit = config.get(CONF_UNITS) - # hass.async_create_task(discovery.async_load_platform( - # hass, 'sensor', DOMAIN, {'unit': unit}, config)) - # - # # port mapping - # port_mapping = config.get(CONF_ENABLE_PORT_MAPPING) - # if not port_mapping: - # return True - # - # # determine ports - # internal_port = hass.http.server_port - # ports = config.get(CONF_PORTS) - # if ports is None: - # ports = {CONF_HASS: internal_port} - # - # registered = [] - # async def register_port_mappings(event): - # """(Re-)register the port mapping.""" - # from async_upnp_client import UpnpError - # for internal, external in ports.items(): - # if internal == CONF_HASS: - # internal = internal_port - # try: - # await igd_device.async_add_port_mapping(remote_host=None, - # external_port=external, - # protocol='TCP', - # internal_port=internal, - # internal_client=ip_address(host), - # enabled=True, - # description='Home Assistant', - # lease_duration=None) - # registered.append(external) - # _LOGGER.debug("external %s -> %s @ %s", external, internal, host) - # except UpnpError as error: - # _LOGGER.error(error) - # hass.components.persistent_notification.create( - # 'ERROR: TCP port {} is already mapped in your router.' - # '
Please disable port_mapping in the upnp ' - # 'configuration section.
' - # 'You will need to restart hass after fixing.' - # ''.format(external), - # title=NOTIFICATION_TITLE, - # notification_id=NOTIFICATION_ID) - # - # async def deregister_port_mappings(event): - # """De-register the port mapping.""" - # tasks = [igd_device.async_delete_port_mapping(remote_host=None, - # external_port=external, - # protocol='TCP') - # for external in registered] - # if tasks: - # await asyncio.wait(tasks) - # - # await register_port_mappings(None) - # hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, deregister_port_mappings) - # - # return True - -async def async_setup_entry(hass, entry): +# config flow +async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigEntry): """Set up a bridge from a config entry.""" - _LOGGER.debug('async_setup_entry, title: %s, data: %s', entry.title, entry.data) + _LOGGER.debug('async_setup_entry: title: %s, data: %s', config_entry.title, config_entry.data) + + data = config_entry.data + ssdp_description = data['ssdp_description'] + + # build IGD device + try: + igd_device = await _async_create_igd_device(hass, ssdp_description) + except (asyncio.TimeoutError, aiohttp.ClientError): + raise PlatformNotReady() + + _store_device(hass, igd_device.udn, igd_device) + + # port forward + if data.get(CONF_PORT_FORWARD): + await _async_create_port_forward(hass, igd_device) - # port mapping? # sensors + if data.get(CONF_SENSORS): + discovery_info = { + 'unit': 'MBytes', + 'udn': data['udn'], + } + hass_config = config_entry.data + hass.async_create_task(discovery.async_load_platform( + hass, 'sensor', DOMAIN, discovery_info, hass_config)) + + async def unload_entry(event): + """Unload entry on quit.""" + await async_unload_entry(hass, config_entry) + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, unload_entry) return True -async def async_unload_entry(hass, entry): + +async def async_unload_entry(hass: HomeAssistantType, config_entry: ConfigEntry): """Unload a config entry.""" + _LOGGER.debug('async_unload_entry: title: %s, data: %s', config_entry.title, config_entry.data) + data = config_entry.data + udn = data['udn'] + igd_device = _get_device(hass, udn) + # port forward + if data.get(CONF_PORT_FORWARD): + _LOGGER.debug('Removing port forward for: %s', igd_device) + _async_remove_port_forward(hass, igd_device) + # sensors + if data.get(CONF_SENSORS): + # XXX TODO: remove sensors + pass + + return True diff --git a/homeassistant/components/igd/config_flow.py b/homeassistant/components/igd/config_flow.py index 2eb1aae5b80..9ccbe79a35f 100644 --- a/homeassistant/components/igd/config_flow.py +++ b/homeassistant/components/igd/config_flow.py @@ -1,40 +1,19 @@ """Config flow for IGD.""" from homeassistant import config_entries, data_entry_flow from homeassistant.core import callback -from homeassistant.helpers import config_validation as cv import voluptuous as vol from .const import DOMAIN from .const import LOGGER as _LOGGER - @callback -def configured_hosts(hass): - """Return a set of the configured hosts.""" - return set(entry.data['ssdp_url'] - for entry in hass.config_entries.async_entries(DOMAIN)) - - -async def _get_igd_device(hass, ssdp_url): - """.""" - # build requester - from async_upnp_client.aiohttp import AiohttpSessionRequester - session = async_get_clientsession(hass) - requester = AiohttpSessionRequester(session, True) - - # create upnp device - from async_upnp_client import UpnpFactory - factory = UpnpFactory(requester, disable_state_variable_validation=True) - try: - upnp_device = await factory.async_create_device(ssdp_url) - except (asyncio.TimeoutError, aiohttp.ClientError): - raise PlatformNotReady() - - # wrap with IgdDevice - from async_upnp_client.igd import IgdDevice - igd_device = IgdDevice(upnp_device, None) - return igd_device +def configured_udns(hass): + """Get all configured UDNs.""" + return [ + entry.data['udn'] + for entry in hass.config_entries.async_entries(DOMAIN) + ] @config_entries.HANDLERS.register(DOMAIN) @@ -43,61 +22,114 @@ class IgdFlowHandler(data_entry_flow.FlowHandler): VERSION = 1 - # def __init__(self): - # """Initializer.""" - # self.host = None + def __init__(self): + """Initializer.""" + pass - # flow: 1. detection/user adding - # 2. question: port forward? sensors? - # 3. add it! + @property + def _discovereds(self): + """Get all discovered entries.""" + if DOMAIN not in self.hass.data: + _LOGGER.debug('DOMAIN not in hass.data') + if 'discovered' not in self.hass.data.get(DOMAIN, {}): + _LOGGER.debug('discovered not in hass.data[DOMAIN]') - async def async_step_user(self, user_input=None): - _LOGGER.debug('async_step_user: %s', user_input) - return await self.async_abort(reason='todo') + return self.hass.data.get(DOMAIN, {}).get('discovered', {}) + + def _store_discovery_info(self, discovery_info): + """Add discovery info.""" + udn = discovery_info['udn'] + if DOMAIN not in self.hass.data: + _LOGGER.debug('DOMAIN not in hass.data') + self.hass.data[DOMAIN] = self.hass.data.get(DOMAIN, {}) + if 'discovered' not in self.hass.data[DOMAIN]: + _LOGGER.debug('Creating new discovered: %s', self.hass.data[DOMAIN]) + self.hass.data[DOMAIN]['discovered'] = self.hass.data[DOMAIN].get('discovered', {}) + self.hass.data[DOMAIN]['discovered'][udn] = discovery_info async def async_step_discovery(self, discovery_info): - """Handle a discovered IGD. + """ + Handle a discovered IGD. This flow is triggered by the discovery component. It will check if the host is already configured and delegate to the import step if not. """ - _LOGGER.debug('async_step_discovery: %s', discovery_info) + _LOGGER.debug('async_step_discovery %s: %s', id(self), discovery_info) - ssdp_url = discovery_info['ssdp_description'] - return await self.async_step_options({ - 'ssdp_url': ssdp_url, - }) + # ensure not already discovered/configured + udn = discovery_info['udn'] + if udn in configured_udns(self.hass): + _LOGGER.debug('Already configured: %s', discovery_info) + return self.async_abort(reason='already_configured') - async def async_step_options(self, user_options): - """.""" - _LOGGER.debug('async_step_options: %s', user_options) - if user_options and \ - 'sensors' in user_options and \ - 'port_forward' in user_options: - return await self.async_step_import(user_options) + # store discovered device + self._store_discovery_info(discovery_info) + + # abort --> not showing up in discovered things + # return self.async_abort(reason='user_input_required') + + # user -> showing up in discovered things + return await self.async_step_user() + + async def async_step_user(self, user_input=None): + """Manual set up.""" + _LOGGER.debug('async_step_user %s: %s', id(self), user_input) + + # if user input given, handle it + user_input = user_input or {} + if 'igd_host' in user_input: + if not user_input['sensors'] and not user_input['port_forward']: + _LOGGER.debug('Aborting, no sensors and no portforward') + return self.async_abort(reason='no_sensors_or_port_forward') + + configured_hosts = [ + entry['host'] + for entry in self._discovereds.values() + if entry['udn'] in configured_udns(self.hass) + ] + if user_input['igd_host'] in configured_hosts: + return self.async_abort(reason='already_configured') + + return await self._async_save(user_input) + + # let user choose from all discovered IGDs + _LOGGER.debug('Discovered devices: %s', self._discovereds) + igd_hosts = [ + entry['host'] + for entry in self._discovereds.values() + if entry['udn'] not in configured_udns(self.hass) + ] + if not igd_hosts: + return self.async_abort(reason='no_devices_discovered') return self.async_show_form( - step_id='options', + step_id='user', data_schema=vol.Schema({ - vol.Required('sensors'): cv.boolean, - vol.Required('port_forward'): cv.boolean, - # vol.Optional('ssdp_url', default=user_options['ssdp_url']): cv.url, + vol.Required('igd_host'): vol.In(igd_hosts), + vol.Required('sensors'): bool, + vol.Required('port_forward'): bool, }) ) - async def async_step_import(self, import_info): - """Import a IGD as new entry.""" - _LOGGER.debug('async_step_import: %s', import_info) + async def _async_save(self, import_info): + """Store IGD as new entry.""" + _LOGGER.debug('async_step_import %s: %s', id(self), import_info) + + # ensure we know the host + igd_host = import_info['igd_host'] + discovery_infos = [info + for info in self._discovereds.values() + if info['host'] == igd_host] + if not discovery_infos: + return self.async_abort(reason='host_not_found') + discovery_info = discovery_infos[0] - ssdp_url = import_info['ssdp_url'] - try: - igd_device = await _get_igd_device(self.hass, ssdp_url) # try it to see if it works - except: - pass return self.async_create_entry( - title=igd_device.name, + title=discovery_info['name'], data={ - 'ssdp_url': ssdp_url, - 'udn': igd_device.udn, - } - ) \ No newline at end of file + 'ssdp_description': discovery_info['ssdp_description'], + 'udn': discovery_info['udn'], + 'sensors': import_info['sensors'], + 'port_forward': import_info['port_forward'], + }, + ) diff --git a/homeassistant/components/igd/const.py b/homeassistant/components/igd/const.py index a933497bd3e..d1d92f7ccb5 100644 --- a/homeassistant/components/igd/const.py +++ b/homeassistant/components/igd/const.py @@ -3,3 +3,5 @@ import logging DOMAIN = 'igd' LOGGER = logging.getLogger('homeassistant.components.igd') +CONF_PORT_FORWARD = 'port_forward' +CONF_SENSORS = 'sensors' diff --git a/homeassistant/components/igd/strings.json b/homeassistant/components/igd/strings.json index b88ed5c7968..bd0d2a9b7c0 100644 --- a/homeassistant/components/igd/strings.json +++ b/homeassistant/components/igd/strings.json @@ -2,23 +2,26 @@ "config": { "title": "IGD", "step": { - "options": { - "title": "Extra configuration options for the IGD", + "init": { + "title": "IGD" + }, + "user": { + "title": "Configuration options for the IGD", "data":{ "sensors": "Add traffic in/out sensors", "port_forward": "Enable port forward for Home Assistant\nOnly enable this when your Home Assistant is password protected!", - "ssdp_ur": "SSDP URL", + "ssdp_url": "SSDP URL", + "udn": "UDN", + "name": "Name" } - }, - "import": { - "title": "Link with IGD", - "description": "Setup the IGD" } }, "error": { }, "abort": { + "no_devices_discovered": "No IGDs discovered", "already_configured": "IGD is already configured", + "no_sensors_or_port_forward": "Enable at least sensors or Port forward", "no_igds": "No IGDs discovered", "todo": "TODO" } diff --git a/homeassistant/components/sensor/igd.py b/homeassistant/components/sensor/igd.py index 53692a3432f..1d0402520df 100644 --- a/homeassistant/components/sensor/igd.py +++ b/homeassistant/components/sensor/igd.py @@ -6,26 +6,29 @@ https://home-assistant.io/components/sensor.upnp/ """ import logging -from homeassistant.components.igd import DATA_IGD, UNITS +from homeassistant.components import history +from homeassistant.components.igd import DOMAIN, UNITS from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) -DEPENDENCIES = ['igd'] +DEPENDENCIES = ['igd', 'history'] -BYTES_RECEIVED = 1 -BYTES_SENT = 2 -PACKETS_RECEIVED = 3 -PACKETS_SENT = 4 +BYTES_RECEIVED = 'bytes_received' +BYTES_SENT = 'bytes_sent' +PACKETS_RECEIVED = 'packets_received' +PACKETS_SENT = 'packets_sent' # sensor_type: [friendly_name, convert_unit, icon] SENSOR_TYPES = { - BYTES_RECEIVED: ['received bytes', True, 'mdi:server-network'], - BYTES_SENT: ['sent bytes', True, 'mdi:server-network'], - PACKETS_RECEIVED: ['packets received', False, 'mdi:server-network'], - PACKETS_SENT: ['packets sent', False, 'mdi:server-network'], + BYTES_RECEIVED: ['bytes received', True, 'mdi:server-network', float], + BYTES_SENT: ['bytes sent', True, 'mdi:server-network', float], + PACKETS_RECEIVED: ['packets received', False, 'mdi:server-network', int], + PACKETS_SENT: ['packets sent', False, 'mdi:server-network', int], } +OVERFLOW_AT = 2**32 + async def async_setup_platform(hass, config, async_add_devices, discovery_info=None): @@ -33,11 +36,12 @@ async def async_setup_platform(hass, config, async_add_devices, if discovery_info is None: return - device = hass.data[DATA_IGD]['device'] + udn = discovery_info['udn'] + device = hass.data[DOMAIN]['devices'][udn] unit = discovery_info['unit'] async_add_devices([ IGDSensor(device, t, unit if SENSOR_TYPES[t][1] else '#') - for t in SENSOR_TYPES], True) + for t in SENSOR_TYPES]) class IGDSensor(Entity): @@ -51,6 +55,7 @@ class IGDSensor(Entity): self.unit_factor = UNITS[unit] if unit in UNITS else 1 self._name = 'IGD {}'.format(SENSOR_TYPES[sensor_type][0]) self._state = None + self._last_value = None @property def name(self): @@ -60,9 +65,14 @@ class IGDSensor(Entity): @property def state(self): """Return the state of the device.""" - if self._state: - return format(float(self._state) / self.unit_factor, '.1f') - return self._state + if self._state is None: + return None + + coercer = SENSOR_TYPES[self.type][3] + if coercer == int: + return format(self._state) + + return format(self._state / self.unit_factor, '.1f') @property def icon(self): @@ -76,11 +86,85 @@ class IGDSensor(Entity): async def async_update(self): """Get the latest information from the IGD.""" + new_value = 0 if self.type == BYTES_RECEIVED: - self._state = await self._device.async_get_total_bytes_received() + new_value = await self._device.async_get_total_bytes_received() elif self.type == BYTES_SENT: - self._state = await self._device.async_get_total_bytes_sent() + new_value = await self._device.async_get_total_bytes_sent() elif self.type == PACKETS_RECEIVED: - self._state = await self._device.async_get_total_packets_received() + new_value = await self._device.async_get_total_packets_received() elif self.type == PACKETS_SENT: - self._state = await self._device.async_get_total_packets_sent() + new_value = await self._device.async_get_total_packets_sent() + + self._handle_new_value(new_value) + + # _LOGGER.debug('Removing self: %s', self) + # await self.async_remove() # XXX TODO: does not remove from the UI + + @property + def _last_state(self): + """Get the last state reported to hass.""" + states = history.get_last_state_changes(self.hass, 2, self.entity_id) + entity_states = [ + state for state in states[self.entity_id] + if state.state != 'unknown'] + _LOGGER.debug('%s: entity_states: %s', self.entity_id, entity_states) + if not entity_states: + return None + + return entity_states[0] + + @property + def _last_value_from_state(self): + """Get the last value reported to hass.""" + last_state = self._last_state + if not last_state: + _LOGGER.debug('%s: No last state', self.entity_id) + return None + + coercer = SENSOR_TYPES[self.type][3] + try: + state = coercer(float(last_state.state)) * self.unit_factor + except ValueError: + _LOGGER.debug('%s: value error, coercer: %s, state: %s', self.entity_id, coercer, last_state.state) + raise + state = coercer(0.0) + + return state + + def _handle_new_value(self, new_value): + _LOGGER.debug('%s: handle_new_value: state: %s, new_value: %s, last_value: %s', + self.entity_id, self._state, new_value, self._last_value) + + # ❯❯❯ upnp-client --debug --pprint --device http://192.168.178.1/RootDevice.xml call-action WANCIFC/GetTotalBytesReceived + if self.entity_id is None: + # don't know our entity ID yet, do nothing but store value + self._last_value = new_value + return + + if self._last_value is None: + self._last_value = new_value + + if self._state is None: + # try to get the state from history + self._state = self._last_value_from_state or 0 + + _LOGGER.debug('%s: state: %s, last_value: %s', + self.entity_id, self._state, self._last_value) + + # calculate new state + if self._last_value <= new_value: + diff = new_value - self._last_value + else: + # handle overflow + diff = OVERFLOW_AT - self._last_value + if new_value >= 0: + diff += new_value + else: + # some devices don't overflow and start at 0, but somewhere to -2**32 + diff += new_value - -OVERFLOW_AT + + self._state += diff + self._last_value = new_value + _LOGGER.debug('%s: diff: %s, state: %s, last_value: %s', + self.entity_id, diff, self._state, self._last_value) diff --git a/tests/components/igd/__init__.py b/tests/components/igd/__init__.py new file mode 100644 index 00000000000..d6985e0544b --- /dev/null +++ b/tests/components/igd/__init__.py @@ -0,0 +1 @@ +"""Tests for the IGD component.""" \ No newline at end of file diff --git a/tests/components/igd/test_config_flow.py b/tests/components/igd/test_config_flow.py new file mode 100644 index 00000000000..2fe2c60b19d --- /dev/null +++ b/tests/components/igd/test_config_flow.py @@ -0,0 +1,165 @@ +"""Tests for IGD config flow.""" + +from homeassistant.components import igd + +from tests.common import MockConfigEntry + + +async def test_flow_none_discovered(hass): + """Test no device discovered flow.""" + flow = igd.config_flow.IgdFlowHandler() + flow.hass = hass + + result = await flow.async_step_user() + assert result['type'] == 'abort' + assert result['reason'] == 'no_devices_discovered' + + +async def test_flow_already_configured(hass): + """Test device already configured flow.""" + flow = igd.config_flow.IgdFlowHandler() + flow.hass = hass + + # discovered device + udn = 'uuid:device_1' + hass.data[igd.DOMAIN] = { + 'discovered': { + udn: { + 'host': '192.168.1.1', + 'udn': udn, + }, + }, + } + + # configured entry + MockConfigEntry(domain=igd.DOMAIN, data={ + 'udn': udn, + 'host': '192.168.1.1', + }).add_to_hass(hass) + + result = await flow.async_step_user({ + 'igd_host': '192.168.1.1', + 'sensors': True, + 'port_forward': False, + }) + assert result['type'] == 'abort' + assert result['reason'] == 'already_configured' + + +async def test_flow_no_sensors_no_port_forward(hass): + """Test single device, no sensors, no port_forward.""" + flow = igd.config_flow.IgdFlowHandler() + flow.hass = hass + + # discovered device + udn = 'uuid:device_1' + hass.data[igd.DOMAIN] = { + 'discovered': { + udn: { + 'host': '192.168.1.1', + 'udn': udn, + }, + }, + } + + # configured entry + MockConfigEntry(domain=igd.DOMAIN, data={ + 'udn': udn, + 'host': '192.168.1.1', + }).add_to_hass(hass) + + result = await flow.async_step_user({ + 'igd_host': '192.168.1.1', + 'sensors': False, + 'port_forward': False, + }) + assert result['type'] == 'abort' + assert result['reason'] == 'no_sensors_or_port_forward' + + +async def test_flow_discovered_form(hass): + """Test single device discovered, show form flow.""" + flow = igd.config_flow.IgdFlowHandler() + flow.hass = hass + + # discovered device + udn = 'uuid:device_1' + hass.data[igd.DOMAIN] = { + 'discovered': { + udn: { + 'host': '192.168.1.1', + 'udn': udn, + }, + }, + } + + result = await flow.async_step_user() + assert result['type'] == 'form' + assert result['step_id'] == 'user' + + +async def test_flow_two_discovered_form(hass): + """Test single device discovered, show form flow.""" + flow = igd.config_flow.IgdFlowHandler() + flow.hass = hass + + # discovered device + udn_1 = 'uuid:device_1' + udn_2 = 'uuid:device_2' + hass.data[igd.DOMAIN] = { + 'discovered': { + udn_1: { + 'host': '192.168.1.1', + 'udn': udn_1, + }, + udn_2: { + 'host': '192.168.2.1', + 'udn': udn_2, + }, + }, + } + + result = await flow.async_step_user() + assert result['type'] == 'form' + assert result['step_id'] == 'user' + assert result['data_schema']({ + 'igd_host': '192.168.1.1', + 'sensors': True, + 'port_forward': False, + }) + assert result['data_schema']({ + 'igd_host': '192.168.2.1', + 'sensors': True, + 'port_forward': False, + }) + + +async def test_config_entry_created(hass): + flow = igd.config_flow.IgdFlowHandler() + flow.hass = hass + + # discovered device + udn = 'uuid:device_1' + hass.data[igd.DOMAIN] = { + 'discovered': { + udn: { + 'name': 'Test device 1', + 'host': '192.168.1.1', + 'ssdp_description': 'http://192.168.1.1/desc.xml', + 'udn': udn, + }, + }, + } + + result = await flow.async_step_user({ + 'igd_host': '192.168.1.1', + 'sensors': True, + 'port_forward': False, + }) + assert result['data'] == { + 'port_forward': False, + 'sensors': True, + 'ssdp_description': 'http://192.168.1.1/desc.xml', + 'udn': 'uuid:device_1', + } + assert result['title'] == 'Test device 1' diff --git a/tests/components/igd/test_init.py b/tests/components/igd/test_init.py new file mode 100644 index 00000000000..4405cc10999 --- /dev/null +++ b/tests/components/igd/test_init.py @@ -0,0 +1,41 @@ +"""Test IGD setup process.""" + +from unittest.mock import patch, MagicMock + +from homeassistant.setup import async_setup_component +from homeassistant.components import igd +from homeassistant.const import EVENT_HOMEASSISTANT_STOP + +from tests.common import MockConfigEntry +from tests.common import mock_coro + + +async def test_async_setup_entry_port_forward_created(hass): + """Test async_setup_entry.""" + + udn = 'uuid:device_1' + entry = MockConfigEntry(domain=igd.DOMAIN, data={ + 'ssdp_description': 'http://192.168.1.1/desc.xml', + 'udn': udn, + 'sensors': False, + 'port_forward': True, + }) + + # ensure hass.http is available + await async_setup_component(hass, 'igd') + + mock_igd_device = MagicMock() + mock_igd_device.udn = udn + mock_igd_device.async_add_port_mapping.return_value = mock_coro() + mock_igd_device.async_remove_port_mapping.return_value = mock_coro() + with patch.object(igd, '_async_create_igd_device') as mock_create_device: + mock_create_device.return_value = mock_coro(return_value=mock_igd_device) + with patch('homeassistant.components.igd.get_local_ip', return_value='192.168.1.10'): + assert await igd.async_setup_entry(hass, entry) is True + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + + assert hass.data[igd.DOMAIN]['devices'][udn] == mock_igd_device + assert len(mock_igd_device.async_add_port_mapping.mock_calls) > 0 + assert len(mock_igd_device.async_delete_port_mapping.mock_calls) > 0 diff --git a/tests/components/test_igd.py b/tests/components/test_igd.py deleted file mode 100644 index eb42b3127f6..00000000000 --- a/tests/components/test_igd.py +++ /dev/null @@ -1,183 +0,0 @@ -"""Test the UPNP component.""" -from collections import OrderedDict -from unittest.mock import patch, MagicMock - -import pytest - -from homeassistant.const import EVENT_HOMEASSISTANT_STOP -from homeassistant.setup import async_setup_component -from homeassistant.components.igd import IP_SERVICE, DATA_IGD - - -class MockService(MagicMock): - """Mock upnp IP service.""" - - async def add_port_mapping(self, *args, **kwargs): - """Original function.""" - self.mock_add_port_mapping(*args, **kwargs) - - async def delete_port_mapping(self, *args, **kwargs): - """Original function.""" - self.mock_delete_port_mapping(*args, **kwargs) - - -class MockDevice(MagicMock): - """Mock upnp device.""" - - def find_first_service(self, *args, **kwargs): - """Original function.""" - self._service = MockService() - return self._service - - def peep_first_service(self): - """Access Mock first service.""" - return self._service - - -class MockResp(MagicMock): - """Mock upnp msearch response.""" - - async def get_device(self, *args, **kwargs): - """Original function.""" - device = MockDevice() - service = {'serviceType': IP_SERVICE} - device.services = [service] - return device - - -@pytest.fixture -def mock_msearch_first(*args, **kwargs): - """Wrap async mock msearch_first.""" - async def async_mock_msearch_first(*args, **kwargs): - """Mock msearch_first.""" - return MockResp(*args, **kwargs) - - with patch('pyupnp_async.msearch_first', new=async_mock_msearch_first): - yield - - -@pytest.fixture -def mock_async_exception(*args, **kwargs): - """Wrap async mock exception.""" - async def async_mock_exception(*args, **kwargs): - return Exception - - with patch('pyupnp_async.msearch_first', new=async_mock_exception): - yield - - -@pytest.fixture -def mock_local_ip(): - """Mock get_local_ip.""" - with patch('homeassistant.components.upnp.get_local_ip', - return_value='192.168.0.10'): - yield - - -async def test_setup_fail_if_no_ip(hass): - """Test setup fails if we can't find a local IP.""" - with patch('homeassistant.components.upnp.get_local_ip', - return_value='127.0.0.1'): - result = await async_setup_component(hass, 'upnp', { - 'upnp': {} - }) - - assert not result - - -async def test_setup_fail_if_cannot_select_igd(hass, - mock_local_ip, - mock_async_exception): - """Test setup fails if we can't find an UPnP IGD.""" - result = await async_setup_component(hass, 'upnp', { - 'upnp': {} - }) - - assert not result - - -async def test_setup_succeeds_if_specify_ip(hass, mock_msearch_first): - """Test setup succeeds if we specify IP and can't find a local IP.""" - with patch('homeassistant.components.upnp.get_local_ip', - return_value='127.0.0.1'): - result = await async_setup_component(hass, 'upnp', { - 'upnp': { - 'local_ip': '192.168.0.10', - 'port_mapping': 'True' - } - }) - - assert result - mock_service = hass.data[DATA_IGD].peep_first_service() - assert len(mock_service.mock_add_port_mapping.mock_calls) == 1 - mock_service.mock_add_port_mapping.assert_called_once_with( - 8123, 8123, '192.168.0.10', 'TCP', desc='Home Assistant') - - -async def test_no_config_maps_hass_local_to_remote_port(hass, - mock_local_ip, - mock_msearch_first): - """Test by default we map local to remote port.""" - result = await async_setup_component(hass, 'upnp', { - 'upnp': { - 'port_mapping': 'True' - } - }) - - assert result - mock_service = hass.data[DATA_IGD].peep_first_service() - assert len(mock_service.mock_add_port_mapping.mock_calls) == 1 - mock_service.mock_add_port_mapping.assert_called_once_with( - 8123, 8123, '192.168.0.10', 'TCP', desc='Home Assistant') - - -async def test_map_hass_to_remote_port(hass, - mock_local_ip, - mock_msearch_first): - """Test mapping hass to remote port.""" - result = await async_setup_component(hass, 'upnp', { - 'upnp': { - 'port_mapping': 'True', - 'ports': { - 'hass': 1000 - } - } - }) - - assert result - mock_service = hass.data[DATA_IGD].peep_first_service() - assert len(mock_service.mock_add_port_mapping.mock_calls) == 1 - mock_service.mock_add_port_mapping.assert_called_once_with( - 8123, 1000, '192.168.0.10', 'TCP', desc='Home Assistant') - - -async def test_map_internal_to_remote_ports(hass, - mock_local_ip, - mock_msearch_first): - """Test mapping local to remote ports.""" - ports = OrderedDict() - ports['hass'] = 1000 - ports[1883] = 3883 - - result = await async_setup_component(hass, 'upnp', { - 'upnp': { - 'port_mapping': 'True', - 'ports': ports - } - }) - - assert result - mock_service = hass.data[DATA_IGD].peep_first_service() - assert len(mock_service.mock_add_port_mapping.mock_calls) == 2 - - mock_service.mock_add_port_mapping.assert_any_call( - 8123, 1000, '192.168.0.10', 'TCP', desc='Home Assistant') - mock_service.mock_add_port_mapping.assert_any_call( - 1883, 3883, '192.168.0.10', 'TCP', desc='Home Assistant') - - hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) - await hass.async_block_till_done() - assert len(mock_service.mock_delete_port_mapping.mock_calls) == 2 - - mock_service.mock_delete_port_mapping.assert_any_call(1000, 'TCP') - mock_service.mock_delete_port_mapping.assert_any_call(3883, 'TCP') From 20879726b0b4fe37af926ca1423c5f7cd43cb986 Mon Sep 17 00:00:00 2001 From: Steven Looman Date: Thu, 30 Aug 2018 16:38:43 +0200 Subject: [PATCH 005/247] Working on IGD --- homeassistant/components/igd/__init__.py | 147 ++++++++++---------- homeassistant/components/igd/config_flow.py | 60 ++++---- homeassistant/components/igd/const.py | 4 +- homeassistant/components/sensor/igd.py | 18 +-- requirements_all.txt | 4 +- requirements_test_all.txt | 3 - tests/components/igd/__init__.py | 2 +- tests/components/igd/test_config_flow.py | 84 +++++++++-- tests/components/igd/test_init.py | 104 +++++++++++++- 9 files changed, 284 insertions(+), 142 deletions(-) diff --git a/homeassistant/components/igd/__init__.py b/homeassistant/components/igd/__init__.py index b932a4f8055..191b8c1fb72 100644 --- a/homeassistant/components/igd/__init__.py +++ b/homeassistant/components/igd/__init__.py @@ -4,52 +4,34 @@ Will open a port in your router for Home Assistant and provide statistics. For more details about this component, please refer to the documentation at https://home-assistant.io/components/upnp/ """ -# XXX TODO: -# + flow: -# + discovery -# + adding device -# + removing device -# - configured: -# - adding -# - sensors: -# + adding -# + handle overflow -# - removing -# - port forward: -# - adding -# - removing -# - shutdown +import asyncio from ipaddress import IPv4Address from ipaddress import ip_address -import aiohttp -import asyncio +import aiohttp import voluptuous as vol -from homeassistant import config_entries from homeassistant.config_entries import ConfigEntry from homeassistant.const import EVENT_HOMEASSISTANT_STOP -from homeassistant.const import CONF_URL from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers import config_validation as cv from homeassistant.helpers import discovery from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.helpers.typing import ConfigType, HomeAssistantType from homeassistant.util import get_local_ip +from homeassistant.components.discovery import DOMAIN as DISCOVERY_DOMAIN -from .config_flow import configured_udns -from .const import CONF_PORT_FORWARD, CONF_SENSORS +from .const import CONF_ENABLE_PORT_MAPPING, CONF_ENABLE_SENSORS from .const import DOMAIN from .const import LOGGER as _LOGGER REQUIREMENTS = ['async-upnp-client==0.12.4'] -DEPENDENCIES = ['http'] +DEPENDENCIES = ['http'] # ,'discovery'] CONF_LOCAL_IP = 'local_ip' -CONF_ENABLE_PORT_MAPPING = 'port_mapping' CONF_PORTS = 'ports' CONF_UNITS = 'unit' CONF_HASS = 'hass' @@ -66,17 +48,16 @@ UNITS = { CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ - vol.Required(CONF_URL): cv.url, - vol.Optional(CONF_ENABLE_PORT_MAPPING, default=True): cv.boolean, - vol.Optional(CONF_UNITS, default="MBytes"): vol.In(UNITS), + vol.Optional(CONF_ENABLE_PORT_MAPPING, default=False): cv.boolean, + vol.Optional(CONF_ENABLE_SENSORS, default=True): cv.boolean, vol.Optional(CONF_LOCAL_IP): vol.All(ip_address, cv.string), - vol.Optional(CONF_PORTS): - vol.Schema({vol.Any(CONF_HASS, cv.positive_int): cv.positive_int}) + vol.Optional(CONF_UNITS, default="MBytes"): vol.In(UNITS), }), }, extra=vol.ALLOW_EXTRA) -async def _async_create_igd_device(hass: HomeAssistantType, ssdp_description: str): +async def _async_create_igd_device(hass: HomeAssistantType, + ssdp_description: str): """.""" # build requester from async_upnp_client.aiohttp import AiohttpSessionRequester @@ -111,19 +92,22 @@ def _get_device(hass: HomeAssistantType, udn): return hass.data[DOMAIN]['devices'][udn] -async def _async_create_port_forward(hass: HomeAssistantType, igd_device): - """Create a port forward.""" - _LOGGER.debug('Creating port forward: %s', igd_device) - +async def _async_add_port_mapping(hass: HomeAssistantType, + igd_device, + local_ip=None): + """Create a port mapping.""" # determine local ip, ensure sane IP - local_ip = get_local_ip() + if local_ip is None: + local_ip = get_local_ip() + if local_ip == '127.0.0.1': - _LOGGER.warning('Could not create port forward, our IP is 127.0.0.1') + _LOGGER.warning('Could not create port mapping, our IP is 127.0.0.1') return False local_ip = IPv4Address(local_ip) # create port mapping port = hass.http.server_port + _LOGGER.debug('Creating port mapping %s:%s:%s (TCP)', port, local_ip, port) await igd_device.async_add_port_mapping(remote_host=None, external_port=port, protocol='TCP', @@ -136,48 +120,52 @@ async def _async_create_port_forward(hass: HomeAssistantType, igd_device): return True -async def _async_remove_port_forward(hass: HomeAssistantType, igd_device): - """Remove a port forward.""" - _LOGGER.debug('Removing port forward: %s', igd_device) - - # remove port mapping +async def _async_delete_port_mapping(hass: HomeAssistantType, igd_device): + """Remove a port mapping.""" port = hass.http.server_port - await igd_device.async_remove_port_mapping(remote_host=None, + await igd_device.async_delete_port_mapping(remote_host=None, external_port=port, protocol='TCP') # config -async def async_setup(hass: HomeAssistantType, config): +async def async_setup(hass: HomeAssistantType, config: ConfigType): """Register a port mapping for Home Assistant via UPnP.""" - _LOGGER.debug('async_setup: config: %s', config) - conf = config.get(DOMAIN, {}) + # defaults + hass.data[DOMAIN] = { + 'auto_config': { + 'active': False, + 'port_forward': False, + 'sensors': False, + } + } - hass.data[DOMAIN] = hass.data.get(DOMAIN, {}) - configured = configured_udns(hass) - _LOGGER.debug('configured: %s', configured) + # ensure sane config + if DOMAIN not in config: + return False - # if no ssdp given: take any discovered - by flow - IGD entry - # if none discovered, raise PlatformNotReady - # if ssdp given: use the SSDP + if DISCOVERY_DOMAIN not in config: + _LOGGER.warning('IGD needs discovery, please enable it') + return False - igds = [] # XXX TODO - for igd_conf in igds: - hass.async_add_job(hass.config_entries.flow.async_init( - DOMAIN, context={'source': config_entries.SOURCE_IMPORT}, - data={ - 'ssdp_description': igd_conf['ssdp_description'], - } - )) + igd_config = config[DOMAIN] + if CONF_LOCAL_IP in igd_config: + hass.data[DOMAIN]['local_ip'] = igd_config[CONF_LOCAL_IP] + + hass.data[DOMAIN]['auto_config'] = { + 'active': True, + 'port_forward': igd_config[CONF_ENABLE_PORT_MAPPING], + 'sensors': igd_config[CONF_ENABLE_SENSORS], + } + _LOGGER.debug('Enabled auto_config: %s', hass.data[DOMAIN]['auto_config']) return True # config flow -async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistantType, + config_entry: ConfigEntry): """Set up a bridge from a config entry.""" - _LOGGER.debug('async_setup_entry: title: %s, data: %s', config_entry.title, config_entry.data) - data = config_entry.data ssdp_description = data['ssdp_description'] @@ -189,44 +177,49 @@ async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigEntry): _store_device(hass, igd_device.udn, igd_device) - # port forward - if data.get(CONF_PORT_FORWARD): - await _async_create_port_forward(hass, igd_device) + # port mapping + if data.get(CONF_ENABLE_PORT_MAPPING): + local_ip = hass.data[DOMAIN].get('local_ip') + await _async_add_port_mapping(hass, igd_device, local_ip=local_ip) # sensors - if data.get(CONF_SENSORS): + if data.get(CONF_ENABLE_SENSORS): discovery_info = { 'unit': 'MBytes', 'udn': data['udn'], } hass_config = config_entry.data - hass.async_create_task(discovery.async_load_platform( - hass, 'sensor', DOMAIN, discovery_info, hass_config)) + hass.async_create_task( + discovery.async_load_platform( + hass, 'sensor', DOMAIN, discovery_info, hass_config)) async def unload_entry(event): """Unload entry on quit.""" await async_unload_entry(hass, config_entry) - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, unload_entry) return True -async def async_unload_entry(hass: HomeAssistantType, config_entry: ConfigEntry): +async def async_unload_entry(hass: HomeAssistantType, + config_entry: ConfigEntry): """Unload a config entry.""" - _LOGGER.debug('async_unload_entry: title: %s, data: %s', config_entry.title, config_entry.data) data = config_entry.data udn = data['udn'] - igd_device = _get_device(hass, udn) - # port forward - if data.get(CONF_PORT_FORWARD): - _LOGGER.debug('Removing port forward for: %s', igd_device) - _async_remove_port_forward(hass, igd_device) + igd_device = _get_device(hass, udn) + if igd_device is None: + return True + + # port mapping + if data.get(CONF_ENABLE_PORT_MAPPING): + await _async_delete_port_mapping(hass, igd_device) # sensors - if data.get(CONF_SENSORS): + if data.get(CONF_ENABLE_SENSORS): # XXX TODO: remove sensors pass + _store_device(hass, udn, None) + return True diff --git a/homeassistant/components/igd/config_flow.py b/homeassistant/components/igd/config_flow.py index 9ccbe79a35f..be6cf9f5f93 100644 --- a/homeassistant/components/igd/config_flow.py +++ b/homeassistant/components/igd/config_flow.py @@ -1,11 +1,12 @@ """Config flow for IGD.""" +import voluptuous as vol + from homeassistant import config_entries, data_entry_flow from homeassistant.core import callback -import voluptuous as vol - +from .const import CONF_ENABLE_PORT_MAPPING, CONF_ENABLE_SENSORS from .const import DOMAIN -from .const import LOGGER as _LOGGER + @callback def configured_udns(hass): @@ -29,24 +30,23 @@ class IgdFlowHandler(data_entry_flow.FlowHandler): @property def _discovereds(self): """Get all discovered entries.""" - if DOMAIN not in self.hass.data: - _LOGGER.debug('DOMAIN not in hass.data') - if 'discovered' not in self.hass.data.get(DOMAIN, {}): - _LOGGER.debug('discovered not in hass.data[DOMAIN]') - return self.hass.data.get(DOMAIN, {}).get('discovered', {}) def _store_discovery_info(self, discovery_info): """Add discovery info.""" udn = discovery_info['udn'] - if DOMAIN not in self.hass.data: - _LOGGER.debug('DOMAIN not in hass.data') self.hass.data[DOMAIN] = self.hass.data.get(DOMAIN, {}) - if 'discovered' not in self.hass.data[DOMAIN]: - _LOGGER.debug('Creating new discovered: %s', self.hass.data[DOMAIN]) - self.hass.data[DOMAIN]['discovered'] = self.hass.data[DOMAIN].get('discovered', {}) + self.hass.data[DOMAIN]['discovered'] = \ + self.hass.data[DOMAIN].get('discovered', {}) self.hass.data[DOMAIN]['discovered'][udn] = discovery_info + def _auto_config_settings(self): + """Check if auto_config has been enabled.""" + self.hass.data[DOMAIN] = self.hass.data.get(DOMAIN, {}) + return self.hass.data[DOMAIN].get('auto_config', { + 'active': False, + }) + async def async_step_discovery(self, discovery_info): """ Handle a discovered IGD. @@ -54,32 +54,33 @@ class IgdFlowHandler(data_entry_flow.FlowHandler): This flow is triggered by the discovery component. It will check if the host is already configured and delegate to the import step if not. """ - _LOGGER.debug('async_step_discovery %s: %s', id(self), discovery_info) - # ensure not already discovered/configured udn = discovery_info['udn'] if udn in configured_udns(self.hass): - _LOGGER.debug('Already configured: %s', discovery_info) return self.async_abort(reason='already_configured') # store discovered device self._store_discovery_info(discovery_info) - # abort --> not showing up in discovered things - # return self.async_abort(reason='user_input_required') + # auto config? + auto_config = self._auto_config_settings() + if auto_config['active']: + import_info = { + 'igd_host': discovery_info['host'], + 'sensors': auto_config['sensors'], + 'port_forward': auto_config['port_forward'], + } + + return await self._async_save_entry(import_info) - # user -> showing up in discovered things return await self.async_step_user() async def async_step_user(self, user_input=None): """Manual set up.""" - _LOGGER.debug('async_step_user %s: %s', id(self), user_input) - # if user input given, handle it user_input = user_input or {} if 'igd_host' in user_input: if not user_input['sensors'] and not user_input['port_forward']: - _LOGGER.debug('Aborting, no sensors and no portforward') return self.async_abort(reason='no_sensors_or_port_forward') configured_hosts = [ @@ -90,10 +91,9 @@ class IgdFlowHandler(data_entry_flow.FlowHandler): if user_input['igd_host'] in configured_hosts: return self.async_abort(reason='already_configured') - return await self._async_save(user_input) + return await self._async_save_entry(user_input) # let user choose from all discovered IGDs - _LOGGER.debug('Discovered devices: %s', self._discovereds) igd_hosts = [ entry['host'] for entry in self._discovereds.values() @@ -111,10 +111,12 @@ class IgdFlowHandler(data_entry_flow.FlowHandler): }) ) - async def _async_save(self, import_info): - """Store IGD as new entry.""" - _LOGGER.debug('async_step_import %s: %s', id(self), import_info) + async def async_step_import(self, import_info): + """Import a new IGD as a config entry.""" + return await self._async_save_entry(import_info) + async def _async_save_entry(self, import_info): + """Store IGD as new entry.""" # ensure we know the host igd_host = import_info['igd_host'] discovery_infos = [info @@ -129,7 +131,7 @@ class IgdFlowHandler(data_entry_flow.FlowHandler): data={ 'ssdp_description': discovery_info['ssdp_description'], 'udn': discovery_info['udn'], - 'sensors': import_info['sensors'], - 'port_forward': import_info['port_forward'], + CONF_ENABLE_SENSORS: import_info['sensors'], + CONF_ENABLE_PORT_MAPPING: import_info['port_forward'], }, ) diff --git a/homeassistant/components/igd/const.py b/homeassistant/components/igd/const.py index d1d92f7ccb5..c849e627757 100644 --- a/homeassistant/components/igd/const.py +++ b/homeassistant/components/igd/const.py @@ -3,5 +3,5 @@ import logging DOMAIN = 'igd' LOGGER = logging.getLogger('homeassistant.components.igd') -CONF_PORT_FORWARD = 'port_forward' -CONF_SENSORS = 'sensors' +CONF_ENABLE_PORT_MAPPING = 'port_forward' +CONF_ENABLE_SENSORS = 'sensors' diff --git a/homeassistant/components/sensor/igd.py b/homeassistant/components/sensor/igd.py index 1d0402520df..edd936600e3 100644 --- a/homeassistant/components/sensor/igd.py +++ b/homeassistant/components/sensor/igd.py @@ -1,15 +1,17 @@ """ -Support for UPnP Sensors (IGD). +Support for IGD Sensors. For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.upnp/ +https://home-assistant.io/components/sensor.igd/ """ +# pylint: disable=invalid-name import logging from homeassistant.components import history from homeassistant.components.igd import DOMAIN, UNITS from homeassistant.helpers.entity import Entity + _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['igd', 'history'] @@ -98,9 +100,6 @@ class IGDSensor(Entity): self._handle_new_value(new_value) - # _LOGGER.debug('Removing self: %s', self) - # await self.async_remove() # XXX TODO: does not remove from the UI - @property def _last_state(self): """Get the last state reported to hass.""" @@ -126,17 +125,11 @@ class IGDSensor(Entity): try: state = coercer(float(last_state.state)) * self.unit_factor except ValueError: - _LOGGER.debug('%s: value error, coercer: %s, state: %s', self.entity_id, coercer, last_state.state) - raise state = coercer(0.0) return state def _handle_new_value(self, new_value): - _LOGGER.debug('%s: handle_new_value: state: %s, new_value: %s, last_value: %s', - self.entity_id, self._state, new_value, self._last_value) - - # ❯❯❯ upnp-client --debug --pprint --device http://192.168.178.1/RootDevice.xml call-action WANCIFC/GetTotalBytesReceived if self.entity_id is None: # don't know our entity ID yet, do nothing but store value self._last_value = new_value @@ -161,7 +154,8 @@ class IGDSensor(Entity): if new_value >= 0: diff += new_value else: - # some devices don't overflow and start at 0, but somewhere to -2**32 + # some devices don't overflow and start at 0, + # but somewhere to -2**32 diff += new_value - -OVERFLOW_AT self._state += diff diff --git a/requirements_all.txt b/requirements_all.txt index ef89fb096da..97336bbae50 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -141,6 +141,7 @@ apns2==0.3.0 # homeassistant.components.asterisk_mbox asterisk_mbox==0.4.0 +# homeassistant.components.igd # homeassistant.components.media_player.dlna_dmr async-upnp-client==0.12.4 @@ -1183,9 +1184,6 @@ pytrafikverket==0.1.5.8 # homeassistant.components.device_tracker.unifi pyunifi==2.13 -# homeassistant.components.upnp -pyupnp-async==0.1.1.1 - # homeassistant.components.binary_sensor.uptimerobot pyuptimerobot==0.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2415d661e29..bba6b5ca46a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -177,9 +177,6 @@ pytradfri[async]==5.5.1 # homeassistant.components.device_tracker.unifi pyunifi==2.13 -# homeassistant.components.upnp -pyupnp-async==0.1.1.1 - # homeassistant.components.notify.html5 pywebpush==1.6.0 diff --git a/tests/components/igd/__init__.py b/tests/components/igd/__init__.py index d6985e0544b..4fcc4167e5b 100644 --- a/tests/components/igd/__init__.py +++ b/tests/components/igd/__init__.py @@ -1 +1 @@ -"""Tests for the IGD component.""" \ No newline at end of file +"""Tests for the IGD component.""" diff --git a/tests/components/igd/test_config_flow.py b/tests/components/igd/test_config_flow.py index 2fe2c60b19d..5530e172dae 100644 --- a/tests/components/igd/test_config_flow.py +++ b/tests/components/igd/test_config_flow.py @@ -1,13 +1,14 @@ """Tests for IGD config flow.""" from homeassistant.components import igd +from homeassistant.components.igd import config_flow as igd_config_flow from tests.common import MockConfigEntry async def test_flow_none_discovered(hass): """Test no device discovered flow.""" - flow = igd.config_flow.IgdFlowHandler() + flow = igd_config_flow.IgdFlowHandler() flow.hass = hass result = await flow.async_step_user() @@ -17,7 +18,7 @@ async def test_flow_none_discovered(hass): async def test_flow_already_configured(hass): """Test device already configured flow.""" - flow = igd.config_flow.IgdFlowHandler() + flow = igd_config_flow.IgdFlowHandler() flow.hass = hass # discovered device @@ -48,7 +49,7 @@ async def test_flow_already_configured(hass): async def test_flow_no_sensors_no_port_forward(hass): """Test single device, no sensors, no port_forward.""" - flow = igd.config_flow.IgdFlowHandler() + flow = igd_config_flow.IgdFlowHandler() flow.hass = hass # discovered device @@ -79,7 +80,7 @@ async def test_flow_no_sensors_no_port_forward(hass): async def test_flow_discovered_form(hass): """Test single device discovered, show form flow.""" - flow = igd.config_flow.IgdFlowHandler() + flow = igd_config_flow.IgdFlowHandler() flow.hass = hass # discovered device @@ -100,7 +101,7 @@ async def test_flow_discovered_form(hass): async def test_flow_two_discovered_form(hass): """Test single device discovered, show form flow.""" - flow = igd.config_flow.IgdFlowHandler() + flow = igd_config_flow.IgdFlowHandler() flow.hass = hass # discovered device @@ -135,18 +136,18 @@ async def test_flow_two_discovered_form(hass): async def test_config_entry_created(hass): - flow = igd.config_flow.IgdFlowHandler() + """Test config entry is created.""" + flow = igd_config_flow.IgdFlowHandler() flow.hass = hass # discovered device - udn = 'uuid:device_1' hass.data[igd.DOMAIN] = { 'discovered': { - udn: { + 'uuid:device_1': { 'name': 'Test device 1', 'host': '192.168.1.1', 'ssdp_description': 'http://192.168.1.1/desc.xml', - 'udn': udn, + 'udn': 'uuid:device_1', }, }, } @@ -156,6 +157,7 @@ async def test_config_entry_created(hass): 'sensors': True, 'port_forward': False, }) + assert result['type'] == 'create_entry' assert result['data'] == { 'port_forward': False, 'sensors': True, @@ -163,3 +165,67 @@ async def test_config_entry_created(hass): 'udn': 'uuid:device_1', } assert result['title'] == 'Test device 1' + + +async def test_flow_discovery_auto_config_sensors(hass): + """Test creation of device with auto_config.""" + flow = igd_config_flow.IgdFlowHandler() + flow.hass = hass + + # auto_config active + hass.data[igd.DOMAIN] = { + 'auto_config': { + 'active': True, + 'port_forward': False, + 'sensors': True, + }, + } + + # discovered device + result = await flow.async_step_discovery({ + 'name': 'Test device 1', + 'host': '192.168.1.1', + 'ssdp_description': 'http://192.168.1.1/desc.xml', + 'udn': 'uuid:device_1', + }) + + assert result['type'] == 'create_entry' + assert result['data'] == { + 'port_forward': False, + 'sensors': True, + 'ssdp_description': 'http://192.168.1.1/desc.xml', + 'udn': 'uuid:device_1', + } + assert result['title'] == 'Test device 1' + + +async def test_flow_discovery_auto_config_sensors_port_forward(hass): + """Test creation of device with auto_config, with port forward.""" + flow = igd_config_flow.IgdFlowHandler() + flow.hass = hass + + # auto_config active, with port_forward + hass.data[igd.DOMAIN] = { + 'auto_config': { + 'active': True, + 'port_forward': True, + 'sensors': True, + }, + } + + # discovered device + result = await flow.async_step_discovery({ + 'name': 'Test device 1', + 'host': '192.168.1.1', + 'ssdp_description': 'http://192.168.1.1/desc.xml', + 'udn': 'uuid:device_1', + }) + + assert result['type'] == 'create_entry' + assert result['data'] == { + 'port_forward': True, + 'sensors': True, + 'ssdp_description': 'http://192.168.1.1/desc.xml', + 'udn': 'uuid:device_1', + } + assert result['title'] == 'Test device 1' diff --git a/tests/components/igd/test_init.py b/tests/components/igd/test_init.py index 4405cc10999..285755c687f 100644 --- a/tests/components/igd/test_init.py +++ b/tests/components/igd/test_init.py @@ -10,9 +10,96 @@ from tests.common import MockConfigEntry from tests.common import mock_coro -async def test_async_setup_entry_port_forward_created(hass): - """Test async_setup_entry.""" +async def test_async_setup_no_auto_config(hass): + """Test async_setup.""" + # setup component, enable auto_config + await async_setup_component(hass, 'igd') + assert hass.data[igd.DOMAIN]['auto_config'] == { + 'active': False, + 'port_forward': False, + 'sensors': False, + } + + +async def test_async_setup_auto_config(hass): + """Test async_setup.""" + # setup component, enable auto_config + await async_setup_component(hass, 'igd', {'igd': {}, 'discovery': {}}) + + assert hass.data[igd.DOMAIN]['auto_config'] == { + 'active': True, + 'port_forward': False, + 'sensors': True, + } + + +async def test_async_setup_auto_config_port_forward(hass): + """Test async_setup.""" + # setup component, enable auto_config + await async_setup_component(hass, 'igd', { + 'igd': {'port_forward': True}, + 'discovery': {}}) + + assert hass.data[igd.DOMAIN]['auto_config'] == { + 'active': True, + 'port_forward': True, + 'sensors': True, + } + + +async def test_async_setup_auto_config_no_sensors(hass): + """Test async_setup.""" + # setup component, enable auto_config + await async_setup_component(hass, 'igd', { + 'igd': {'sensors': False}, + 'discovery': {}}) + + assert hass.data[igd.DOMAIN]['auto_config'] == { + 'active': True, + 'port_forward': False, + 'sensors': False, + } + + +async def test_async_setup_entry_default(hass): + """Test async_setup_entry.""" + udn = 'uuid:device_1' + entry = MockConfigEntry(domain=igd.DOMAIN, data={ + 'ssdp_description': 'http://192.168.1.1/desc.xml', + 'udn': udn, + 'sensors': True, + 'port_forward': False, + }) + + # ensure hass.http is available + await async_setup_component(hass, 'igd') + + # mock async_upnp_client.igd.IgdDevice + mock_igd_device = MagicMock() + mock_igd_device.udn = udn + mock_igd_device.async_add_port_mapping.return_value = mock_coro() + mock_igd_device.async_delete_port_mapping.return_value = mock_coro() + with patch.object(igd, '_async_create_igd_device') as mock_create_device: + mock_create_device.return_value = mock_coro( + return_value=mock_igd_device) + with patch('homeassistant.components.igd.get_local_ip', + return_value='192.168.1.10'): + assert await igd.async_setup_entry(hass, entry) is True + + # ensure device is stored/used + assert hass.data[igd.DOMAIN]['devices'][udn] == mock_igd_device + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + + assert hass.data[igd.DOMAIN]['devices'][udn] is None + assert len(mock_igd_device.async_add_port_mapping.mock_calls) == 0 + assert len(mock_igd_device.async_delete_port_mapping.mock_calls) == 0 + + +async def test_async_setup_entry_port_forward(hass): + """Test async_setup_entry.""" udn = 'uuid:device_1' entry = MockConfigEntry(domain=igd.DOMAIN, data={ 'ssdp_description': 'http://192.168.1.1/desc.xml', @@ -27,15 +114,20 @@ async def test_async_setup_entry_port_forward_created(hass): mock_igd_device = MagicMock() mock_igd_device.udn = udn mock_igd_device.async_add_port_mapping.return_value = mock_coro() - mock_igd_device.async_remove_port_mapping.return_value = mock_coro() + mock_igd_device.async_delete_port_mapping.return_value = mock_coro() with patch.object(igd, '_async_create_igd_device') as mock_create_device: - mock_create_device.return_value = mock_coro(return_value=mock_igd_device) - with patch('homeassistant.components.igd.get_local_ip', return_value='192.168.1.10'): + mock_create_device.return_value = mock_coro( + return_value=mock_igd_device) + with patch('homeassistant.components.igd.get_local_ip', + return_value='192.168.1.10'): assert await igd.async_setup_entry(hass, entry) is True + # ensure device is stored/used + assert hass.data[igd.DOMAIN]['devices'][udn] == mock_igd_device + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) await hass.async_block_till_done() - assert hass.data[igd.DOMAIN]['devices'][udn] == mock_igd_device + assert hass.data[igd.DOMAIN]['devices'][udn] is None assert len(mock_igd_device.async_add_port_mapping.mock_calls) > 0 assert len(mock_igd_device.async_delete_port_mapping.mock_calls) > 0 From 38318ed15ff3886c7e734cd7c922261367cad91e Mon Sep 17 00:00:00 2001 From: Steven Looman Date: Sat, 1 Sep 2018 10:09:22 +0200 Subject: [PATCH 006/247] Ensure config_flow is imported --- homeassistant/components/igd/__init__.py | 1 + homeassistant/config_entries.py | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/igd/__init__.py b/homeassistant/components/igd/__init__.py index 191b8c1fb72..305ac993b87 100644 --- a/homeassistant/components/igd/__init__.py +++ b/homeassistant/components/igd/__init__.py @@ -26,6 +26,7 @@ from homeassistant.components.discovery import DOMAIN as DISCOVERY_DOMAIN from .const import CONF_ENABLE_PORT_MAPPING, CONF_ENABLE_SENSORS from .const import DOMAIN from .const import LOGGER as _LOGGER +import homeassistant.components.igd.config_flow # register the handler REQUIREMENTS = ['async-upnp-client==0.12.4'] diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index a52484b6941..93c904d3b91 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -414,7 +414,6 @@ class ConfigEntries: Handler key is the domain of the component that we want to set up. """ component = getattr(self.hass.components, handler_key) - _LOGGER.debug('Handler key: %s', handler_key) handler = HANDLERS.get(handler_key) if handler is None: From c9e34e236da147d206ce66ec5d181269e622212d Mon Sep 17 00:00:00 2001 From: Steven Looman Date: Sat, 1 Sep 2018 10:14:21 +0200 Subject: [PATCH 007/247] Prevent error message when component is actually ok --- homeassistant/components/igd/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/igd/__init__.py b/homeassistant/components/igd/__init__.py index 305ac993b87..4f2e8dc7e71 100644 --- a/homeassistant/components/igd/__init__.py +++ b/homeassistant/components/igd/__init__.py @@ -143,7 +143,7 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType): # ensure sane config if DOMAIN not in config: - return False + return True if DISCOVERY_DOMAIN not in config: _LOGGER.warning('IGD needs discovery, please enable it') From 50f63ed4c5f5fdd2ab247b4a7a2ef39d34d6a856 Mon Sep 17 00:00:00 2001 From: Steven Looman Date: Sat, 1 Sep 2018 18:13:45 +0200 Subject: [PATCH 008/247] Fix not being able to re-add IGD when removed --- .coveragerc | 2 +- .../components/igd/.translations/en.json | 8 +- .../components/igd/.translations/nl.json | 8 +- homeassistant/components/igd/__init__.py | 26 +- homeassistant/components/igd/config_flow.py | 33 +- homeassistant/components/igd/strings.json | 8 +- homeassistant/components/sensor/igd.py | 290 ++++++++++++------ tests/components/igd/test_config_flow.py | 16 +- 8 files changed, 245 insertions(+), 146 deletions(-) diff --git a/.coveragerc b/.coveragerc index 39c31e4e40b..078b03f21d2 100644 --- a/.coveragerc +++ b/.coveragerc @@ -674,6 +674,7 @@ omit = homeassistant/components/sensor/haveibeenpwned.py homeassistant/components/sensor/hp_ilo.py homeassistant/components/sensor/htu21d.py + homeassistant/components/sensor/igd.py homeassistant/components/sensor/imap_email_content.py homeassistant/components/sensor/imap.py homeassistant/components/sensor/influxdb.py @@ -757,7 +758,6 @@ omit = homeassistant/components/sensor/travisci.py homeassistant/components/sensor/twitch.py homeassistant/components/sensor/uber.py - homeassistant/components/sensor/upnp.py homeassistant/components/sensor/ups.py homeassistant/components/sensor/uscis.py homeassistant/components/sensor/vasttrafik.py diff --git a/homeassistant/components/igd/.translations/en.json b/homeassistant/components/igd/.translations/en.json index bd0d2a9b7c0..e3b55a9dfd3 100644 --- a/homeassistant/components/igd/.translations/en.json +++ b/homeassistant/components/igd/.translations/en.json @@ -8,11 +8,9 @@ "user": { "title": "Configuration options for the IGD", "data":{ - "sensors": "Add traffic in/out sensors", - "port_forward": "Enable port forward for Home Assistant\nOnly enable this when your Home Assistant is password protected!", - "ssdp_url": "SSDP URL", - "udn": "UDN", - "name": "Name" + "igd": "IGD", + "sensors": "Add traffic sensors", + "port_forward": "Enable port forward for Home Assistant
Only enable this when your Home Assistant is password protected!" } } }, diff --git a/homeassistant/components/igd/.translations/nl.json b/homeassistant/components/igd/.translations/nl.json index 06f9122678c..2cd5e6b1651 100644 --- a/homeassistant/components/igd/.translations/nl.json +++ b/homeassistant/components/igd/.translations/nl.json @@ -8,11 +8,9 @@ "user": { "title": "Extra configuratie options voor IGD", "data":{ - "sensors": "Verkeer in/out sensors", - "port_forward": "Maak port-forward voor Home Assistant\nZet dit alleen aan wanneer uw Home Assistant een wachtwoord heeft!", - "ssdp_url": "SSDP URL", - "udn": "UDN", - "name": "Naam" + "igd": "IGD", + "sensors": "Verkeer sensors toevoegen", + "port_forward": "Maak port-forward voor Home Assistant
Zet dit alleen aan wanneer uw Home Assistant een wachtwoord heeft!" } } }, diff --git a/homeassistant/components/igd/__init__.py b/homeassistant/components/igd/__init__.py index 4f2e8dc7e71..44c625ed87c 100644 --- a/homeassistant/components/igd/__init__.py +++ b/homeassistant/components/igd/__init__.py @@ -34,25 +34,16 @@ DEPENDENCIES = ['http'] # ,'discovery'] CONF_LOCAL_IP = 'local_ip' CONF_PORTS = 'ports' -CONF_UNITS = 'unit' CONF_HASS = 'hass' NOTIFICATION_ID = 'igd_notification' NOTIFICATION_TITLE = 'UPnP/IGD Setup' -UNITS = { - "Bytes": 1, - "KBytes": 1024, - "MBytes": 1024**2, - "GBytes": 1024**3, -} - CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ vol.Optional(CONF_ENABLE_PORT_MAPPING, default=False): cv.boolean, vol.Optional(CONF_ENABLE_SENSORS, default=True): cv.boolean, vol.Optional(CONF_LOCAL_IP): vol.All(ip_address, cv.string), - vol.Optional(CONF_UNITS, default="MBytes"): vol.In(UNITS), }), }, extra=vol.ALLOW_EXTRA) @@ -133,13 +124,13 @@ async def _async_delete_port_mapping(hass: HomeAssistantType, igd_device): async def async_setup(hass: HomeAssistantType, config: ConfigType): """Register a port mapping for Home Assistant via UPnP.""" # defaults - hass.data[DOMAIN] = { - 'auto_config': { + hass.data[DOMAIN] = hass.data.get(DOMAIN, {}) + if 'auto_config' not in hass.data[DOMAIN]: + hass.data[DOMAIN]['auto_config'] = { 'active': False, 'port_forward': False, 'sensors': False, } - } # ensure sane config if DOMAIN not in config: @@ -167,6 +158,8 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType): async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigEntry): """Set up a bridge from a config entry.""" + _LOGGER.debug('async_setup_entry: %s, %s', config_entry, config_entry.entry_id) + data = config_entry.data ssdp_description = data['ssdp_description'] @@ -186,7 +179,6 @@ async def async_setup_entry(hass: HomeAssistantType, # sensors if data.get(CONF_ENABLE_SENSORS): discovery_info = { - 'unit': 'MBytes', 'udn': data['udn'], } hass_config = config_entry.data @@ -205,6 +197,10 @@ async def async_setup_entry(hass: HomeAssistantType, async def async_unload_entry(hass: HomeAssistantType, config_entry: ConfigEntry): """Unload a config entry.""" + _LOGGER.debug('async_unload_entry: %s, entry_id: %s, data: %s', config_entry, config_entry.entry_id, config_entry.data) + for entry in hass.config_entries._entries: + _LOGGER.debug('%s: %s: %s', entry, entry.entry_id, entry.data) + data = config_entry.data udn = data['udn'] @@ -221,6 +217,10 @@ async def async_unload_entry(hass: HomeAssistantType, # XXX TODO: remove sensors pass + # clear stored device _store_device(hass, udn, None) + # XXX TODO: remove config entry + #await hass.config_entries.async_remove(config_entry.entry_id) + return True diff --git a/homeassistant/components/igd/config_flow.py b/homeassistant/components/igd/config_flow.py index be6cf9f5f93..6269e92f9c3 100644 --- a/homeassistant/components/igd/config_flow.py +++ b/homeassistant/components/igd/config_flow.py @@ -6,6 +6,7 @@ from homeassistant.core import callback from .const import CONF_ENABLE_PORT_MAPPING, CONF_ENABLE_SENSORS from .const import DOMAIN +from .const import LOGGER as _LOGGER @callback @@ -60,13 +61,15 @@ class IgdFlowHandler(data_entry_flow.FlowHandler): return self.async_abort(reason='already_configured') # store discovered device + discovery_info['friendly_name'] = \ + '{} ({})'.format(discovery_info['host'], discovery_info['name']) self._store_discovery_info(discovery_info) # auto config? auto_config = self._auto_config_settings() if auto_config['active']: import_info = { - 'igd_host': discovery_info['host'], + 'name': discovery_info['friendly_name'], 'sensors': auto_config['sensors'], 'port_forward': auto_config['port_forward'], } @@ -79,35 +82,37 @@ class IgdFlowHandler(data_entry_flow.FlowHandler): """Manual set up.""" # if user input given, handle it user_input = user_input or {} - if 'igd_host' in user_input: + if 'name' in user_input: if not user_input['sensors'] and not user_input['port_forward']: return self.async_abort(reason='no_sensors_or_port_forward') - configured_hosts = [ - entry['host'] + # ensure nto already configured + configured_igds = [ + entry['friendly_name'] for entry in self._discovereds.values() if entry['udn'] in configured_udns(self.hass) ] - if user_input['igd_host'] in configured_hosts: + _LOGGER.debug('Configured IGDs: %s', configured_igds) + if user_input['name'] in configured_igds: return self.async_abort(reason='already_configured') return await self._async_save_entry(user_input) - # let user choose from all discovered IGDs - igd_hosts = [ - entry['host'] + # let user choose from all discovered, non-configured, IGDs + names = [ + entry['friendly_name'] for entry in self._discovereds.values() if entry['udn'] not in configured_udns(self.hass) ] - if not igd_hosts: + if not names: return self.async_abort(reason='no_devices_discovered') return self.async_show_form( step_id='user', data_schema=vol.Schema({ - vol.Required('igd_host'): vol.In(igd_hosts), - vol.Required('sensors'): bool, - vol.Required('port_forward'): bool, + vol.Required('name'): vol.In(names), + vol.Optional('sensors', default=False): bool, + vol.Optional('port_forward', default=False): bool, }) ) @@ -118,10 +123,10 @@ class IgdFlowHandler(data_entry_flow.FlowHandler): async def _async_save_entry(self, import_info): """Store IGD as new entry.""" # ensure we know the host - igd_host = import_info['igd_host'] + name = import_info['name'] discovery_infos = [info for info in self._discovereds.values() - if info['host'] == igd_host] + if info['friendly_name'] == name] if not discovery_infos: return self.async_abort(reason='host_not_found') discovery_info = discovery_infos[0] diff --git a/homeassistant/components/igd/strings.json b/homeassistant/components/igd/strings.json index bd0d2a9b7c0..e3b55a9dfd3 100644 --- a/homeassistant/components/igd/strings.json +++ b/homeassistant/components/igd/strings.json @@ -8,11 +8,9 @@ "user": { "title": "Configuration options for the IGD", "data":{ - "sensors": "Add traffic in/out sensors", - "port_forward": "Enable port forward for Home Assistant\nOnly enable this when your Home Assistant is password protected!", - "ssdp_url": "SSDP URL", - "udn": "UDN", - "name": "Name" + "igd": "IGD", + "sensors": "Add traffic sensors", + "port_forward": "Enable port forward for Home Assistant
Only enable this when your Home Assistant is password protected!" } } }, diff --git a/homeassistant/components/sensor/igd.py b/homeassistant/components/sensor/igd.py index edd936600e3..63cbdff89fc 100644 --- a/homeassistant/components/sensor/igd.py +++ b/homeassistant/components/sensor/igd.py @@ -5,10 +5,10 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.igd/ """ # pylint: disable=invalid-name +from datetime import datetime import logging -from homeassistant.components import history -from homeassistant.components.igd import DOMAIN, UNITS +from homeassistant.components.igd import DOMAIN from homeassistant.helpers.entity import Entity @@ -21,15 +21,28 @@ BYTES_SENT = 'bytes_sent' PACKETS_RECEIVED = 'packets_received' PACKETS_SENT = 'packets_sent' -# sensor_type: [friendly_name, convert_unit, icon] SENSOR_TYPES = { - BYTES_RECEIVED: ['bytes received', True, 'mdi:server-network', float], - BYTES_SENT: ['bytes sent', True, 'mdi:server-network', float], - PACKETS_RECEIVED: ['packets received', False, 'mdi:server-network', int], - PACKETS_SENT: ['packets sent', False, 'mdi:server-network', int], + BYTES_RECEIVED: { + 'name': 'bytes received', + 'unit': 'bytes', + }, + BYTES_SENT: { + 'name': 'bytes sent', + 'unit': 'bytes', + }, + PACKETS_RECEIVED: { + 'name': 'packets received', + 'unit': '#', + }, + PACKETS_SENT: { + 'name': 'packets sent', + 'unit': '#', + }, } -OVERFLOW_AT = 2**32 +IN = 'received' +OUT = 'sent' +KBYTE = 1024 async def async_setup_platform(hass, config, async_add_devices, @@ -39,126 +52,207 @@ async def async_setup_platform(hass, config, async_add_devices, return udn = discovery_info['udn'] - device = hass.data[DOMAIN]['devices'][udn] - unit = discovery_info['unit'] + igd_device = hass.data[DOMAIN]['devices'][udn] + + # raw sensors async_add_devices([ - IGDSensor(device, t, unit if SENSOR_TYPES[t][1] else '#') - for t in SENSOR_TYPES]) + RawIGDSensor(igd_device, name, sensor_type) + for name, sensor_type in SENSOR_TYPES.items()], + True + ) + + # current traffic reporting + async_add_devices( + [ + KBytePerSecondIGDSensor(igd_device, IN), + KBytePerSecondIGDSensor(igd_device, OUT), + PacketsPerSecondIGDSensor(igd_device, IN), + PacketsPerSecondIGDSensor(igd_device, OUT), + ], True + ) -class IGDSensor(Entity): +class RawIGDSensor(Entity): """Representation of a UPnP IGD sensor.""" - def __init__(self, device, sensor_type, unit=None): + def __init__(self, device, sensor_type_name, sensor_type): """Initialize the IGD sensor.""" self._device = device - self.type = sensor_type - self.unit = unit - self.unit_factor = UNITS[unit] if unit in UNITS else 1 - self._name = 'IGD {}'.format(SENSOR_TYPES[sensor_type][0]) + self._type_name = sensor_type_name + self._type = sensor_type + self._name = 'IGD {}'.format(sensor_type['name']) self._state = None - self._last_value = None @property - def name(self): + def name(self) -> str: """Return the name of the sensor.""" return self._name @property - def state(self): + def state(self) -> str: """Return the state of the device.""" - if self._state is None: - return None - - coercer = SENSOR_TYPES[self.type][3] - if coercer == int: - return format(self._state) - - return format(self._state / self.unit_factor, '.1f') + return self._state @property - def icon(self): + def icon(self) -> str: """Icon to use in the frontend, if any.""" - return SENSOR_TYPES[self.type][2] + return 'mdi:server-network' @property - def unit_of_measurement(self): + def unit_of_measurement(self) -> str: """Return the unit of measurement of this entity, if any.""" - return self.unit + return self._type['unit'] async def async_update(self): """Get the latest information from the IGD.""" - new_value = 0 - if self.type == BYTES_RECEIVED: + _LOGGER.debug('%s: async_update', self) + if self._type_name == BYTES_RECEIVED: + self._state = await self._device.async_get_total_bytes_received() + elif self._type_name == BYTES_SENT: + self._state = await self._device.async_get_total_bytes_sent() + elif self._type_name == PACKETS_RECEIVED: + self._state = await self._device.async_get_total_packets_received() + elif self._type_name == PACKETS_SENT: + self._state = await self._device.async_get_total_packets_sent() + + +class KBytePerSecondIGDSensor(Entity): + """Representation of a KBytes Sent/Received per second sensor.""" + + def __init__(self, device, direction): + """Initializer.""" + self._device = device + self._direction = direction + + self._last_value = None + self._last_update_time = None + self._state = None + + @property + def unique_id(self) -> str: + """Return an unique ID.""" + return '{}:kbytes_{}'.format(self._device.udn, self._direction) + + @property + def name(self) -> str: + """Return the name of the sensor.""" + return '{} kbytes/sec {}'.format(self._device.name, + self._direction) + + @property + def state(self): + """Return the state of the device.""" + return self._state + + @property + def icon(self) -> str: + """Icon to use in the frontend, if any.""" + return 'mdi:server-network' + + @property + def unit_of_measurement(self) -> str: + """Return the unit of measurement of this entity, if any.""" + return 'kbytes/sec' + + def _is_overflowed(self, new_value) -> bool: + """Check if value has overflowed.""" + return new_value < self._last_value + + async def async_update(self): + """Get the latest information from the IGD.""" + _LOGGER.debug('%s: async_update', self) + + if self._direction == IN: new_value = await self._device.async_get_total_bytes_received() - elif self.type == BYTES_SENT: + else: new_value = await self._device.async_get_total_bytes_sent() - elif self.type == PACKETS_RECEIVED: - new_value = await self._device.async_get_total_packets_received() - elif self.type == PACKETS_SENT: - new_value = await self._device.async_get_total_packets_sent() - - self._handle_new_value(new_value) - - @property - def _last_state(self): - """Get the last state reported to hass.""" - states = history.get_last_state_changes(self.hass, 2, self.entity_id) - entity_states = [ - state for state in states[self.entity_id] - if state.state != 'unknown'] - _LOGGER.debug('%s: entity_states: %s', self.entity_id, entity_states) - if not entity_states: - return None - - return entity_states[0] - - @property - def _last_value_from_state(self): - """Get the last value reported to hass.""" - last_state = self._last_state - if not last_state: - _LOGGER.debug('%s: No last state', self.entity_id) - return None - - coercer = SENSOR_TYPES[self.type][3] - try: - state = coercer(float(last_state.state)) * self.unit_factor - except ValueError: - state = coercer(0.0) - - return state - - def _handle_new_value(self, new_value): - if self.entity_id is None: - # don't know our entity ID yet, do nothing but store value - self._last_value = new_value - return if self._last_value is None: self._last_value = new_value + self._last_update_time = datetime.now() + return - if self._state is None: - # try to get the state from history - self._state = self._last_value_from_state or 0 - - _LOGGER.debug('%s: state: %s, last_value: %s', - self.entity_id, self._state, self._last_value) - - # calculate new state - if self._last_value <= new_value: - diff = new_value - self._last_value + now = datetime.now() + if self._is_overflowed(new_value): + _LOGGER.debug('%s: Overflow: old value: %s, new value: %s', + self, self._last_value, new_value) + self._state = None # temporarily report nothing else: - # handle overflow - diff = OVERFLOW_AT - self._last_value - if new_value >= 0: - diff += new_value - else: - # some devices don't overflow and start at 0, - # but somewhere to -2**32 - diff += new_value - -OVERFLOW_AT + delta_time = (now - self._last_update_time).seconds + delta_value = new_value - self._last_value + value = (delta_value / delta_time) / KBYTE + self._state = format(float(value), '.1f') - self._state += diff self._last_value = new_value - _LOGGER.debug('%s: diff: %s, state: %s, last_value: %s', - self.entity_id, diff, self._state, self._last_value) + self._last_update_time = now + + +class PacketsPerSecondIGDSensor(Entity): + """Representation of a Packets Sent/Received per second sensor.""" + + def __init__(self, device, direction): + """Initializer.""" + self._device = device + self._direction = direction + + self._last_value = None + self._last_update_time = None + self._state = None + + @property + def unique_id(self) -> str: + """Return an unique ID.""" + return '{}:packets_{}'.format(self._device.udn, self._direction) + + @property + def name(self) -> str: + """Return the name of the sensor.""" + return '{} packets/sec {}'.format(self._device.name, + self._direction) + + @property + def state(self): + """Return the state of the device.""" + return self._state + + @property + def icon(self) -> str: + """Icon to use in the frontend, if any.""" + return 'mdi:server-network' + + @property + def unit_of_measurement(self) -> str: + """Return the unit of measurement of this entity, if any.""" + return '#/sec' + + def _is_overflowed(self, new_value) -> bool: + """Check if value has overflowed.""" + return new_value < self._last_value + + async def async_update(self): + """Get the latest information from the IGD.""" + _LOGGER.debug('%s: async_update', self) + + if self._direction == IN: + new_value = await self._device.async_get_total_bytes_received() + else: + new_value = await self._device.async_get_total_bytes_sent() + + if self._last_value is None: + self._last_value = new_value + self._last_update_time = datetime.now() + return + + now = datetime.now() + if self._is_overflowed(new_value): + _LOGGER.debug('%s: Overflow: old value: %s, new value: %s', + self, self._last_value, new_value) + self._state = None # temporarily report nothing + else: + delta_time = (now - self._last_update_time).seconds + delta_value = new_value - self._last_value + value = delta_value / delta_time + self._state = format(float(value), '.1f') + + self._last_value = new_value + self._last_update_time = now diff --git a/tests/components/igd/test_config_flow.py b/tests/components/igd/test_config_flow.py index 5530e172dae..4fc103c0772 100644 --- a/tests/components/igd/test_config_flow.py +++ b/tests/components/igd/test_config_flow.py @@ -26,6 +26,7 @@ async def test_flow_already_configured(hass): hass.data[igd.DOMAIN] = { 'discovered': { udn: { + 'friendly_name': '192.168.1.1 (Test device)', 'host': '192.168.1.1', 'udn': udn, }, @@ -39,7 +40,7 @@ async def test_flow_already_configured(hass): }).add_to_hass(hass) result = await flow.async_step_user({ - 'igd_host': '192.168.1.1', + 'name': '192.168.1.1 (Test device)', 'sensors': True, 'port_forward': False, }) @@ -57,6 +58,7 @@ async def test_flow_no_sensors_no_port_forward(hass): hass.data[igd.DOMAIN] = { 'discovered': { udn: { + 'friendly_name': '192.168.1.1 (Test device)', 'host': '192.168.1.1', 'udn': udn, }, @@ -70,7 +72,7 @@ async def test_flow_no_sensors_no_port_forward(hass): }).add_to_hass(hass) result = await flow.async_step_user({ - 'igd_host': '192.168.1.1', + 'name': '192.168.1.1 (Test device)', 'sensors': False, 'port_forward': False, }) @@ -88,6 +90,7 @@ async def test_flow_discovered_form(hass): hass.data[igd.DOMAIN] = { 'discovered': { udn: { + 'friendly_name': '192.168.1.1 (Test device)', 'host': '192.168.1.1', 'udn': udn, }, @@ -110,10 +113,12 @@ async def test_flow_two_discovered_form(hass): hass.data[igd.DOMAIN] = { 'discovered': { udn_1: { + 'friendly_name': '192.168.1.1 (Test device)', 'host': '192.168.1.1', 'udn': udn_1, }, udn_2: { + 'friendly_name': '192.168.2.1 (Test device)', 'host': '192.168.2.1', 'udn': udn_2, }, @@ -124,12 +129,12 @@ async def test_flow_two_discovered_form(hass): assert result['type'] == 'form' assert result['step_id'] == 'user' assert result['data_schema']({ - 'igd_host': '192.168.1.1', + 'name': '192.168.1.1 (Test device)', 'sensors': True, 'port_forward': False, }) assert result['data_schema']({ - 'igd_host': '192.168.2.1', + 'name': '192.168.1.1 (Test device)', 'sensors': True, 'port_forward': False, }) @@ -144,6 +149,7 @@ async def test_config_entry_created(hass): hass.data[igd.DOMAIN] = { 'discovered': { 'uuid:device_1': { + 'friendly_name': '192.168.1.1 (Test device)', 'name': 'Test device 1', 'host': '192.168.1.1', 'ssdp_description': 'http://192.168.1.1/desc.xml', @@ -153,7 +159,7 @@ async def test_config_entry_created(hass): } result = await flow.async_step_user({ - 'igd_host': '192.168.1.1', + 'name': '192.168.1.1 (Test device)', 'sensors': True, 'port_forward': False, }) From 8bec4a55d1de3475d7db1a1dd38e85a796d811a1 Mon Sep 17 00:00:00 2001 From: Steven Looman Date: Sat, 1 Sep 2018 21:20:15 +0200 Subject: [PATCH 009/247] Working on igd --- CODEOWNERS | 1 - .../components/igd/.translations/en.json | 7 +- .../components/igd/.translations/nl.json | 7 +- homeassistant/components/igd/__init__.py | 60 ++-- homeassistant/components/igd/config_flow.py | 75 +++-- homeassistant/components/igd/const.py | 16 ++ homeassistant/components/igd/strings.json | 7 +- homeassistant/components/sensor/igd.py | 257 ++++++++---------- tests/components/igd/test_config_flow.py | 3 + tests/components/igd/test_init.py | 4 +- 10 files changed, 206 insertions(+), 231 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index b86e09a6b72..9015e0fb44c 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -80,7 +80,6 @@ homeassistant/components/sensor/sma.py @kellerza homeassistant/components/sensor/sql.py @dgomes homeassistant/components/sensor/sytadin.py @gautric homeassistant/components/sensor/tibber.py @danielhiversen -homeassistant/components/sensor/upnp.py @dgomes homeassistant/components/sensor/waqi.py @andrey-git homeassistant/components/switch/tplink.py @rytilahti homeassistant/components/vacuum/roomba.py @pschmitt diff --git a/homeassistant/components/igd/.translations/en.json b/homeassistant/components/igd/.translations/en.json index e3b55a9dfd3..3466b6cfa93 100644 --- a/homeassistant/components/igd/.translations/en.json +++ b/homeassistant/components/igd/.translations/en.json @@ -10,7 +10,7 @@ "data":{ "igd": "IGD", "sensors": "Add traffic sensors", - "port_forward": "Enable port forward for Home Assistant
Only enable this when your Home Assistant is password protected!" + "port_forward": "Enable port forward for Home Assistant" } } }, @@ -20,8 +20,7 @@ "no_devices_discovered": "No IGDs discovered", "already_configured": "IGD is already configured", "no_sensors_or_port_forward": "Enable at least sensors or Port forward", - "no_igds": "No IGDs discovered", - "todo": "TODO" + "no_igds": "No IGDs discovered" } } -} \ No newline at end of file +} diff --git a/homeassistant/components/igd/.translations/nl.json b/homeassistant/components/igd/.translations/nl.json index 2cd5e6b1651..5a42f6c3bea 100644 --- a/homeassistant/components/igd/.translations/nl.json +++ b/homeassistant/components/igd/.translations/nl.json @@ -10,7 +10,7 @@ "data":{ "igd": "IGD", "sensors": "Verkeer sensors toevoegen", - "port_forward": "Maak port-forward voor Home Assistant
Zet dit alleen aan wanneer uw Home Assistant een wachtwoord heeft!" + "port_forward": "Maak port-forward voor Home Assistant" } } }, @@ -20,8 +20,7 @@ "no_devices_discovered": "Geen IGDs gevonden", "already_configured": "IGD is reeds geconfigureerd", "no_sensors_or_port_forward": "Kies ten minste sensors of Port forward", - "no_igds": "Geen IGDs gevonden", - "todo": "TODO" + "no_igds": "Geen IGDs gevonden" } } -} \ No newline at end of file +} diff --git a/homeassistant/components/igd/__init__.py b/homeassistant/components/igd/__init__.py index 44c625ed87c..d9c6ad1408c 100644 --- a/homeassistant/components/igd/__init__.py +++ b/homeassistant/components/igd/__init__.py @@ -2,10 +2,9 @@ Will open a port in your router for Home Assistant and provide statistics. For more details about this component, please refer to the documentation at -https://home-assistant.io/components/upnp/ +https://home-assistant.io/components/igd/ """ - import asyncio from ipaddress import IPv4Address from ipaddress import ip_address @@ -22,19 +21,22 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.typing import ConfigType, HomeAssistantType from homeassistant.util import get_local_ip from homeassistant.components.discovery import DOMAIN as DISCOVERY_DOMAIN +import homeassistant.components.igd.config_flow # noqa: 401 -from .const import CONF_ENABLE_PORT_MAPPING, CONF_ENABLE_SENSORS +from .const import ( + CONF_ENABLE_PORT_MAPPING, CONF_ENABLE_SENSORS, + CONF_UDN, CONF_SSDP_DESCRIPTION +) from .const import DOMAIN from .const import LOGGER as _LOGGER -import homeassistant.components.igd.config_flow # register the handler +from .const import ensure_domain_data REQUIREMENTS = ['async-upnp-client==0.12.4'] -DEPENDENCIES = ['http'] # ,'discovery'] +DEPENDENCIES = ['http'] CONF_LOCAL_IP = 'local_ip' CONF_PORTS = 'ports' -CONF_HASS = 'hass' NOTIFICATION_ID = 'igd_notification' NOTIFICATION_TITLE = 'UPnP/IGD Setup' @@ -72,16 +74,15 @@ async def _async_create_igd_device(hass: HomeAssistantType, def _store_device(hass: HomeAssistantType, udn, igd_device): """Store an igd_device by udn.""" - hass.data[DOMAIN] = hass.data.get(DOMAIN, {}) - hass.data[DOMAIN]['devices'] = hass.data[DOMAIN].get('devices', {}) - hass.data[DOMAIN]['devices'][udn] = igd_device + if igd_device is not None: + hass.data[DOMAIN]['devices'][udn] = igd_device + elif udn in hass.data[DOMAIN]['devices']: + del hass.data[DOMAIN]['devices'][udn] def _get_device(hass: HomeAssistantType, udn): """Get an igd_device by udn.""" - hass.data[DOMAIN] = hass.data.get(DOMAIN, {}) - hass.data[DOMAIN]['devices'] = hass.data[DOMAIN].get('devices', {}) - return hass.data[DOMAIN]['devices'][udn] + return hass.data[DOMAIN]['devices'].get(udn) async def _async_add_port_mapping(hass: HomeAssistantType, @@ -123,14 +124,7 @@ async def _async_delete_port_mapping(hass: HomeAssistantType, igd_device): # config async def async_setup(hass: HomeAssistantType, config: ConfigType): """Register a port mapping for Home Assistant via UPnP.""" - # defaults - hass.data[DOMAIN] = hass.data.get(DOMAIN, {}) - if 'auto_config' not in hass.data[DOMAIN]: - hass.data[DOMAIN]['auto_config'] = { - 'active': False, - 'port_forward': False, - 'sensors': False, - } + ensure_domain_data(hass) # ensure sane config if DOMAIN not in config: @@ -158,12 +152,11 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType): async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigEntry): """Set up a bridge from a config entry.""" - _LOGGER.debug('async_setup_entry: %s, %s', config_entry, config_entry.entry_id) - + ensure_domain_data(hass) data = config_entry.data - ssdp_description = data['ssdp_description'] # build IGD device + ssdp_description = data[CONF_SSDP_DESCRIPTION] try: igd_device = await _async_create_igd_device(hass, ssdp_description) except (asyncio.TimeoutError, aiohttp.ClientError): @@ -179,12 +172,11 @@ async def async_setup_entry(hass: HomeAssistantType, # sensors if data.get(CONF_ENABLE_SENSORS): discovery_info = { - 'udn': data['udn'], + 'udn': data[CONF_UDN], } hass_config = config_entry.data - hass.async_create_task( - discovery.async_load_platform( - hass, 'sensor', DOMAIN, discovery_info, hass_config)) + hass.async_create_task(discovery.async_load_platform( + hass, 'sensor', DOMAIN, discovery_info, hass_config)) async def unload_entry(event): """Unload entry on quit.""" @@ -197,12 +189,8 @@ async def async_setup_entry(hass: HomeAssistantType, async def async_unload_entry(hass: HomeAssistantType, config_entry: ConfigEntry): """Unload a config entry.""" - _LOGGER.debug('async_unload_entry: %s, entry_id: %s, data: %s', config_entry, config_entry.entry_id, config_entry.data) - for entry in hass.config_entries._entries: - _LOGGER.debug('%s: %s: %s', entry, entry.entry_id, entry.data) - data = config_entry.data - udn = data['udn'] + udn = data[CONF_UDN] igd_device = _get_device(hass, udn) if igd_device is None: @@ -213,14 +201,10 @@ async def async_unload_entry(hass: HomeAssistantType, await _async_delete_port_mapping(hass, igd_device) # sensors - if data.get(CONF_ENABLE_SENSORS): - # XXX TODO: remove sensors - pass + for sensor in hass.data[DOMAIN]['sensors'].get(udn, []): + await sensor.async_remove() # clear stored device _store_device(hass, udn, None) - # XXX TODO: remove config entry - #await hass.config_entries.async_remove(config_entry.entry_id) - return True diff --git a/homeassistant/components/igd/config_flow.py b/homeassistant/components/igd/config_flow.py index 6269e92f9c3..c66241d3b78 100644 --- a/homeassistant/components/igd/config_flow.py +++ b/homeassistant/components/igd/config_flow.py @@ -1,21 +1,15 @@ """Config flow for IGD.""" import voluptuous as vol -from homeassistant import config_entries, data_entry_flow -from homeassistant.core import callback +from homeassistant import config_entries +from homeassistant import data_entry_flow -from .const import CONF_ENABLE_PORT_MAPPING, CONF_ENABLE_SENSORS +from .const import ( + CONF_ENABLE_PORT_MAPPING, CONF_ENABLE_SENSORS, + CONF_SSDP_DESCRIPTION, CONF_UDN +) from .const import DOMAIN -from .const import LOGGER as _LOGGER - - -@callback -def configured_udns(hass): - """Get all configured UDNs.""" - return [ - entry.data['udn'] - for entry in hass.config_entries.async_entries(DOMAIN) - ] +from .const import ensure_domain_data @config_entries.HANDLERS.register(DOMAIN) @@ -24,29 +18,29 @@ class IgdFlowHandler(data_entry_flow.FlowHandler): VERSION = 1 - def __init__(self): - """Initializer.""" - pass + @property + def _configured_igds(self): + """Get all configured IGDs.""" + return { + entry.data[CONF_UDN]: { + 'udn': entry.data[CONF_UDN], + } + for entry in self.hass.config_entries.async_entries(DOMAIN) + } @property - def _discovereds(self): + def _discovered_igds(self): """Get all discovered entries.""" - return self.hass.data.get(DOMAIN, {}).get('discovered', {}) + return self.hass.data[DOMAIN]['discovered'] def _store_discovery_info(self, discovery_info): """Add discovery info.""" udn = discovery_info['udn'] - self.hass.data[DOMAIN] = self.hass.data.get(DOMAIN, {}) - self.hass.data[DOMAIN]['discovered'] = \ - self.hass.data[DOMAIN].get('discovered', {}) self.hass.data[DOMAIN]['discovered'][udn] = discovery_info def _auto_config_settings(self): """Check if auto_config has been enabled.""" - self.hass.data[DOMAIN] = self.hass.data.get(DOMAIN, {}) - return self.hass.data[DOMAIN].get('auto_config', { - 'active': False, - }) + return self.hass.data[DOMAIN]['auto_config'] async def async_step_discovery(self, discovery_info): """ @@ -55,16 +49,18 @@ class IgdFlowHandler(data_entry_flow.FlowHandler): This flow is triggered by the discovery component. It will check if the host is already configured and delegate to the import step if not. """ - # ensure not already discovered/configured - udn = discovery_info['udn'] - if udn in configured_udns(self.hass): - return self.async_abort(reason='already_configured') + ensure_domain_data(self.hass) # store discovered device discovery_info['friendly_name'] = \ '{} ({})'.format(discovery_info['host'], discovery_info['name']) self._store_discovery_info(discovery_info) + # ensure not already discovered/configured + udn = discovery_info['udn'] + if udn in self._configured_igds: + return self.async_abort(reason='already_configured') + # auto config? auto_config = self._auto_config_settings() if auto_config['active']: @@ -86,14 +82,13 @@ class IgdFlowHandler(data_entry_flow.FlowHandler): if not user_input['sensors'] and not user_input['port_forward']: return self.async_abort(reason='no_sensors_or_port_forward') - # ensure nto already configured - configured_igds = [ + # ensure not already configured + configured_names = [ entry['friendly_name'] - for entry in self._discovereds.values() - if entry['udn'] in configured_udns(self.hass) + for udn, entry in self._discovered_igds.items() + if udn in self._configured_igds ] - _LOGGER.debug('Configured IGDs: %s', configured_igds) - if user_input['name'] in configured_igds: + if user_input['name'] in configured_names: return self.async_abort(reason='already_configured') return await self._async_save_entry(user_input) @@ -101,8 +96,8 @@ class IgdFlowHandler(data_entry_flow.FlowHandler): # let user choose from all discovered, non-configured, IGDs names = [ entry['friendly_name'] - for entry in self._discovereds.values() - if entry['udn'] not in configured_udns(self.hass) + for udn, entry in self._discovered_igds.items() + if udn not in self._configured_igds ] if not names: return self.async_abort(reason='no_devices_discovered') @@ -125,7 +120,7 @@ class IgdFlowHandler(data_entry_flow.FlowHandler): # ensure we know the host name = import_info['name'] discovery_infos = [info - for info in self._discovereds.values() + for info in self._discovered_igds.values() if info['friendly_name'] == name] if not discovery_infos: return self.async_abort(reason='host_not_found') @@ -134,8 +129,8 @@ class IgdFlowHandler(data_entry_flow.FlowHandler): return self.async_create_entry( title=discovery_info['name'], data={ - 'ssdp_description': discovery_info['ssdp_description'], - 'udn': discovery_info['udn'], + CONF_SSDP_DESCRIPTION: discovery_info['ssdp_description'], + CONF_UDN: discovery_info['udn'], CONF_ENABLE_SENSORS: import_info['sensors'], CONF_ENABLE_PORT_MAPPING: import_info['port_forward'], }, diff --git a/homeassistant/components/igd/const.py b/homeassistant/components/igd/const.py index c849e627757..58092cdf38f 100644 --- a/homeassistant/components/igd/const.py +++ b/homeassistant/components/igd/const.py @@ -1,7 +1,23 @@ """Constants for the IGD component.""" import logging + DOMAIN = 'igd' LOGGER = logging.getLogger('homeassistant.components.igd') CONF_ENABLE_PORT_MAPPING = 'port_forward' CONF_ENABLE_SENSORS = 'sensors' +CONF_UDN = 'udn' +CONF_SSDP_DESCRIPTION = 'ssdp_description' + + +def ensure_domain_data(hass): + """Ensure hass.data is filled properly.""" + hass.data[DOMAIN] = hass.data.get(DOMAIN, {}) + hass.data[DOMAIN]['devices'] = hass.data[DOMAIN].get('devices', {}) + hass.data[DOMAIN]['sensors'] = hass.data[DOMAIN].get('sensors', {}) + hass.data[DOMAIN]['discovered'] = hass.data[DOMAIN].get('discovered', {}) + hass.data[DOMAIN]['auto_config'] = hass.data[DOMAIN].get('auto_config', { + 'active': False, + 'port_forward': False, + 'sensors': False, + }) diff --git a/homeassistant/components/igd/strings.json b/homeassistant/components/igd/strings.json index e3b55a9dfd3..3466b6cfa93 100644 --- a/homeassistant/components/igd/strings.json +++ b/homeassistant/components/igd/strings.json @@ -10,7 +10,7 @@ "data":{ "igd": "IGD", "sensors": "Add traffic sensors", - "port_forward": "Enable port forward for Home Assistant
Only enable this when your Home Assistant is password protected!" + "port_forward": "Enable port forward for Home Assistant" } } }, @@ -20,8 +20,7 @@ "no_devices_discovered": "No IGDs discovered", "already_configured": "IGD is already configured", "no_sensors_or_port_forward": "Enable at least sensors or Port forward", - "no_igds": "No IGDs discovered", - "todo": "TODO" + "no_igds": "No IGDs discovered" } } -} \ No newline at end of file +} diff --git a/homeassistant/components/sensor/igd.py b/homeassistant/components/sensor/igd.py index 63cbdff89fc..cb0ceb752b0 100644 --- a/homeassistant/components/sensor/igd.py +++ b/homeassistant/components/sensor/igd.py @@ -32,11 +32,11 @@ SENSOR_TYPES = { }, PACKETS_RECEIVED: { 'name': 'packets received', - 'unit': '#', + 'unit': 'packets', }, PACKETS_SENT: { 'name': 'packets sent', - 'unit': '#', + 'unit': 'packets', }, } @@ -54,22 +54,20 @@ async def async_setup_platform(hass, config, async_add_devices, udn = discovery_info['udn'] igd_device = hass.data[DOMAIN]['devices'][udn] - # raw sensors - async_add_devices([ + # raw sensors + per-second sensors + sensors = [ RawIGDSensor(igd_device, name, sensor_type) - for name, sensor_type in SENSOR_TYPES.items()], - True - ) - - # current traffic reporting - async_add_devices( - [ - KBytePerSecondIGDSensor(igd_device, IN), - KBytePerSecondIGDSensor(igd_device, OUT), - PacketsPerSecondIGDSensor(igd_device, IN), - PacketsPerSecondIGDSensor(igd_device, OUT), - ], True - ) + for name, sensor_type in SENSOR_TYPES.items() + ] + sensors += [ + KBytePerSecondIGDSensor(igd_device, IN), + KBytePerSecondIGDSensor(igd_device, OUT), + PacketsPerSecondIGDSensor(igd_device, IN), + PacketsPerSecondIGDSensor(igd_device, OUT), + ] + hass.data[DOMAIN]['sensors'][udn] = sensors + async_add_devices(sensors, True) + return True class RawIGDSensor(Entity): @@ -80,7 +78,7 @@ class RawIGDSensor(Entity): self._device = device self._type_name = sensor_type_name self._type = sensor_type - self._name = 'IGD {}'.format(sensor_type['name']) + self._name = '{} {}'.format(device.name, sensor_type['name']) self._state = None @property @@ -88,6 +86,11 @@ class RawIGDSensor(Entity): """Return the name of the sensor.""" return self._name + @property + def unique_id(self) -> str: + """Return an unique ID.""" + return '{}_{}'.format(self._device.udn, self._type_name) + @property def state(self) -> str: """Return the state of the device.""" @@ -116,143 +119,121 @@ class RawIGDSensor(Entity): self._state = await self._device.async_get_total_packets_sent() -class KBytePerSecondIGDSensor(Entity): +class PerSecondIGDSensor(Entity): + """Abstract representation of a X Sent/Received per second sensor.""" + + def __init__(self, device, direction): + """Initializer.""" + self._device = device + self._direction = direction + + self._state = None + self._last_value = None + self._last_update_time = None + + @property + def unit(self) -> str: + """Unit we are measuring in.""" + raise NotImplementedError() + + @property + def _async_fetch_value(self): + """Fetch a value from the IGD.""" + raise NotImplementedError() + + @property + def unique_id(self) -> str: + """Return an unique ID.""" + return '{}_{}/sec_{}'.format(self._device.udn, + self.unit, + self._direction) + + @property + def name(self) -> str: + """Return the name of the sensor.""" + return '{} {}/sec {}'.format(self._device.name, + self.unit, + self._direction) + + @property + def icon(self) -> str: + """Icon to use in the frontend, if any.""" + return 'mdi:server-network' + + @property + def unit_of_measurement(self) -> str: + """Return the unit of measurement of this entity, if any.""" + return '{}/sec'.format(self.unit) + + def _is_overflowed(self, new_value) -> bool: + """Check if value has overflowed.""" + return new_value < self._last_value + + async def async_update(self): + """Get the latest information from the IGD.""" + _LOGGER.debug('%s: async_update', self) + new_value = await self._async_fetch_value() + + if self._last_value is None: + self._last_value = new_value + self._last_update_time = datetime.now() + return + + now = datetime.now() + if self._is_overflowed(new_value): + self._state = None # temporarily report nothing + else: + delta_time = (now - self._last_update_time).seconds + delta_value = new_value - self._last_value + self._state = (delta_value / delta_time) + + self._last_value = new_value + self._last_update_time = now + + +class KBytePerSecondIGDSensor(PerSecondIGDSensor): """Representation of a KBytes Sent/Received per second sensor.""" - def __init__(self, device, direction): - """Initializer.""" - self._device = device - self._direction = direction - - self._last_value = None - self._last_update_time = None - self._state = None - @property - def unique_id(self) -> str: - """Return an unique ID.""" - return '{}:kbytes_{}'.format(self._device.udn, self._direction) + def unit(self) -> str: + """Unit we are measuring in.""" + return 'kbyte' - @property - def name(self) -> str: - """Return the name of the sensor.""" - return '{} kbytes/sec {}'.format(self._device.name, - self._direction) + async def _async_fetch_value(self) -> float: + """""" + if self._direction == IN: + return await self._device.async_get_total_bytes_received() + + return await self._device.async_get_total_bytes_sent() @property def state(self): """Return the state of the device.""" - return self._state + if self._state is None: + return None - @property - def icon(self) -> str: - """Icon to use in the frontend, if any.""" - return 'mdi:server-network' - - @property - def unit_of_measurement(self) -> str: - """Return the unit of measurement of this entity, if any.""" - return 'kbytes/sec' - - def _is_overflowed(self, new_value) -> bool: - """Check if value has overflowed.""" - return new_value < self._last_value - - async def async_update(self): - """Get the latest information from the IGD.""" - _LOGGER.debug('%s: async_update', self) - - if self._direction == IN: - new_value = await self._device.async_get_total_bytes_received() - else: - new_value = await self._device.async_get_total_bytes_sent() - - if self._last_value is None: - self._last_value = new_value - self._last_update_time = datetime.now() - return - - now = datetime.now() - if self._is_overflowed(new_value): - _LOGGER.debug('%s: Overflow: old value: %s, new value: %s', - self, self._last_value, new_value) - self._state = None # temporarily report nothing - else: - delta_time = (now - self._last_update_time).seconds - delta_value = new_value - self._last_value - value = (delta_value / delta_time) / KBYTE - self._state = format(float(value), '.1f') - - self._last_value = new_value - self._last_update_time = now + return format(float(self._state / KBYTE), '.1f') -class PacketsPerSecondIGDSensor(Entity): +class PacketsPerSecondIGDSensor(PerSecondIGDSensor): """Representation of a Packets Sent/Received per second sensor.""" - def __init__(self, device, direction): - """Initializer.""" - self._device = device - self._direction = direction - - self._last_value = None - self._last_update_time = None - self._state = None - @property - def unique_id(self) -> str: - """Return an unique ID.""" - return '{}:packets_{}'.format(self._device.udn, self._direction) + def unit(self) -> str: + """Unit we are measuring in.""" + return 'packets' - @property - def name(self) -> str: - """Return the name of the sensor.""" - return '{} packets/sec {}'.format(self._device.name, - self._direction) + async def _async_fetch_value(self) -> float: + """""" + if self._direction == IN: + return await self._device.async_get_total_packets_received() + + return await self._device.async_get_total_packets_sent() @property def state(self): """Return the state of the device.""" - return self._state + if self._state is None: + return None - @property - def icon(self) -> str: - """Icon to use in the frontend, if any.""" - return 'mdi:server-network' - - @property - def unit_of_measurement(self) -> str: - """Return the unit of measurement of this entity, if any.""" - return '#/sec' - - def _is_overflowed(self, new_value) -> bool: - """Check if value has overflowed.""" - return new_value < self._last_value - - async def async_update(self): - """Get the latest information from the IGD.""" - _LOGGER.debug('%s: async_update', self) - - if self._direction == IN: - new_value = await self._device.async_get_total_bytes_received() - else: - new_value = await self._device.async_get_total_bytes_sent() - - if self._last_value is None: - self._last_value = new_value - self._last_update_time = datetime.now() - return - - now = datetime.now() - if self._is_overflowed(new_value): - _LOGGER.debug('%s: Overflow: old value: %s, new value: %s', - self, self._last_value, new_value) - self._state = None # temporarily report nothing - else: - delta_time = (now - self._last_update_time).seconds - delta_value = new_value - self._last_value - value = delta_value / delta_time - self._state = format(float(value), '.1f') - - self._last_value = new_value - self._last_update_time = now + return format(float(self._state), '.1f') diff --git a/tests/components/igd/test_config_flow.py b/tests/components/igd/test_config_flow.py index 4fc103c0772..e02d5630e96 100644 --- a/tests/components/igd/test_config_flow.py +++ b/tests/components/igd/test_config_flow.py @@ -10,6 +10,9 @@ async def test_flow_none_discovered(hass): """Test no device discovered flow.""" flow = igd_config_flow.IgdFlowHandler() flow.hass = hass + hass.data[igd.DOMAIN] = { + 'discovered': {} + } result = await flow.async_step_user() assert result['type'] == 'abort' diff --git a/tests/components/igd/test_init.py b/tests/components/igd/test_init.py index 285755c687f..cd3f5324bb4 100644 --- a/tests/components/igd/test_init.py +++ b/tests/components/igd/test_init.py @@ -93,7 +93,7 @@ async def test_async_setup_entry_default(hass): hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) await hass.async_block_till_done() - assert hass.data[igd.DOMAIN]['devices'][udn] is None + assert udn not in hass.data[igd.DOMAIN]['devices'] assert len(mock_igd_device.async_add_port_mapping.mock_calls) == 0 assert len(mock_igd_device.async_delete_port_mapping.mock_calls) == 0 @@ -128,6 +128,6 @@ async def test_async_setup_entry_port_forward(hass): hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) await hass.async_block_till_done() - assert hass.data[igd.DOMAIN]['devices'][udn] is None + assert udn not in hass.data[igd.DOMAIN]['devices'] assert len(mock_igd_device.async_add_port_mapping.mock_calls) > 0 assert len(mock_igd_device.async_delete_port_mapping.mock_calls) > 0 From a4a175440b23c85382abd59e1963b9e5e994ea29 Mon Sep 17 00:00:00 2001 From: Steven Looman Date: Sun, 2 Sep 2018 16:40:39 +0200 Subject: [PATCH 010/247] Move ensure_domain_data --- homeassistant/components/igd/__init__.py | 3 +-- homeassistant/components/igd/config_flow.py | 14 +++++++++++++- homeassistant/components/igd/const.py | 13 ------------- 3 files changed, 14 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/igd/__init__.py b/homeassistant/components/igd/__init__.py index d9c6ad1408c..37445350fa6 100644 --- a/homeassistant/components/igd/__init__.py +++ b/homeassistant/components/igd/__init__.py @@ -21,7 +21,6 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.typing import ConfigType, HomeAssistantType from homeassistant.util import get_local_ip from homeassistant.components.discovery import DOMAIN as DISCOVERY_DOMAIN -import homeassistant.components.igd.config_flow # noqa: 401 from .const import ( CONF_ENABLE_PORT_MAPPING, CONF_ENABLE_SENSORS, @@ -29,7 +28,7 @@ from .const import ( ) from .const import DOMAIN from .const import LOGGER as _LOGGER -from .const import ensure_domain_data +from .config_flow import ensure_domain_data REQUIREMENTS = ['async-upnp-client==0.12.4'] diff --git a/homeassistant/components/igd/config_flow.py b/homeassistant/components/igd/config_flow.py index c66241d3b78..6f0cec12b5b 100644 --- a/homeassistant/components/igd/config_flow.py +++ b/homeassistant/components/igd/config_flow.py @@ -9,7 +9,19 @@ from .const import ( CONF_SSDP_DESCRIPTION, CONF_UDN ) from .const import DOMAIN -from .const import ensure_domain_data + + +def ensure_domain_data(hass): + """Ensure hass.data is filled properly.""" + hass.data[DOMAIN] = hass.data.get(DOMAIN, {}) + hass.data[DOMAIN]['devices'] = hass.data[DOMAIN].get('devices', {}) + hass.data[DOMAIN]['sensors'] = hass.data[DOMAIN].get('sensors', {}) + hass.data[DOMAIN]['discovered'] = hass.data[DOMAIN].get('discovered', {}) + hass.data[DOMAIN]['auto_config'] = hass.data[DOMAIN].get('auto_config', { + 'active': False, + 'port_forward': False, + 'sensors': False, + }) @config_entries.HANDLERS.register(DOMAIN) diff --git a/homeassistant/components/igd/const.py b/homeassistant/components/igd/const.py index 58092cdf38f..47ff9f11074 100644 --- a/homeassistant/components/igd/const.py +++ b/homeassistant/components/igd/const.py @@ -8,16 +8,3 @@ CONF_ENABLE_PORT_MAPPING = 'port_forward' CONF_ENABLE_SENSORS = 'sensors' CONF_UDN = 'udn' CONF_SSDP_DESCRIPTION = 'ssdp_description' - - -def ensure_domain_data(hass): - """Ensure hass.data is filled properly.""" - hass.data[DOMAIN] = hass.data.get(DOMAIN, {}) - hass.data[DOMAIN]['devices'] = hass.data[DOMAIN].get('devices', {}) - hass.data[DOMAIN]['sensors'] = hass.data[DOMAIN].get('sensors', {}) - hass.data[DOMAIN]['discovered'] = hass.data[DOMAIN].get('discovered', {}) - hass.data[DOMAIN]['auto_config'] = hass.data[DOMAIN].get('auto_config', { - 'active': False, - 'port_forward': False, - 'sensors': False, - }) From 85c40d29ee72c487c0fb288c31d54b9c8a8928d9 Mon Sep 17 00:00:00 2001 From: Steven Looman Date: Sun, 2 Sep 2018 17:03:31 +0200 Subject: [PATCH 011/247] Linting --- homeassistant/components/sensor/igd.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/sensor/igd.py b/homeassistant/components/sensor/igd.py index cb0ceb752b0..908e465aedb 100644 --- a/homeassistant/components/sensor/igd.py +++ b/homeassistant/components/sensor/igd.py @@ -133,7 +133,7 @@ class PerSecondIGDSensor(Entity): @property def unit(self) -> str: - """Unit we are measuring in.""" + """Get unit we are measuring in.""" raise NotImplementedError() @property @@ -196,11 +196,11 @@ class KBytePerSecondIGDSensor(PerSecondIGDSensor): @property def unit(self) -> str: - """Unit we are measuring in.""" + """Get unit we are measuring in.""" return 'kbyte' async def _async_fetch_value(self) -> float: - """""" + """Fetch value from device.""" if self._direction == IN: return await self._device.async_get_total_bytes_received() @@ -220,11 +220,11 @@ class PacketsPerSecondIGDSensor(PerSecondIGDSensor): @property def unit(self) -> str: - """Unit we are measuring in.""" + """Get unit we are measuring in.""" return 'packets' async def _async_fetch_value(self) -> float: - """""" + """Fetch value from device.""" if self._direction == IN: return await self._device.async_get_total_packets_received() From f4a54e2483aa803e0d91d97c459f77c160643b1d Mon Sep 17 00:00:00 2001 From: Steven Looman Date: Mon, 3 Sep 2018 21:16:45 +0200 Subject: [PATCH 012/247] Changes after review --- homeassistant/components/igd/__init__.py | 36 ++++++++++++++---------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/igd/__init__.py b/homeassistant/components/igd/__init__.py index 37445350fa6..4f7bc9606ac 100644 --- a/homeassistant/components/igd/__init__.py +++ b/homeassistant/components/igd/__init__.py @@ -51,7 +51,7 @@ CONFIG_SCHEMA = vol.Schema({ async def _async_create_igd_device(hass: HomeAssistantType, ssdp_description: str): - """.""" + """Create IGD device.""" # build requester from async_upnp_client.aiohttp import AiohttpSessionRequester session = async_get_clientsession(hass) @@ -94,30 +94,36 @@ async def _async_add_port_mapping(hass: HomeAssistantType, if local_ip == '127.0.0.1': _LOGGER.warning('Could not create port mapping, our IP is 127.0.0.1') - return False + return local_ip = IPv4Address(local_ip) # create port mapping + from async_upnp_client import UpnpError port = hass.http.server_port _LOGGER.debug('Creating port mapping %s:%s:%s (TCP)', port, local_ip, port) - await igd_device.async_add_port_mapping(remote_host=None, - external_port=port, - protocol='TCP', - internal_port=port, - internal_client=local_ip, - enabled=True, - description="Home Assistant", - lease_duration=None) - - return True + try: + await igd_device.async_add_port_mapping(remote_host=None, + external_port=port, + protocol='TCP', + internal_port=port, + internal_client=local_ip, + enabled=True, + description="Home Assistant", + lease_duration=None) + except (asyncio.TimeoutError, aiohttp.ClientError, UpnpError): + _LOGGER.warning('Could not add port mapping') async def _async_delete_port_mapping(hass: HomeAssistantType, igd_device): """Remove a port mapping.""" + from async_upnp_client import UpnpError port = hass.http.server_port - await igd_device.async_delete_port_mapping(remote_host=None, - external_port=port, - protocol='TCP') + try: + await igd_device.async_delete_port_mapping(remote_host=None, + external_port=port, + protocol='TCP') + except (asyncio.TimeoutError, aiohttp.ClientError, UpnpError): + _LOGGER.warning('Could not delete port mapping') # config From 6433f2e2e315b9787eb7e80a4ea8aa5f453d239e Mon Sep 17 00:00:00 2001 From: Steven Looman Date: Sat, 8 Sep 2018 00:11:23 +0200 Subject: [PATCH 013/247] Working on igd --- homeassistant/components/igd/__init__.py | 128 +++++-------------- homeassistant/components/igd/config_flow.py | 1 + homeassistant/components/igd/const.py | 9 +- homeassistant/components/igd/device.py | 131 ++++++++++++++++++++ homeassistant/components/sensor/igd.py | 16 ++- tests/components/igd/test_init.py | 104 +++++++++++----- 6 files changed, 255 insertions(+), 134 deletions(-) create mode 100644 homeassistant/components/igd/device.py diff --git a/homeassistant/components/igd/__init__.py b/homeassistant/components/igd/__init__.py index 4f7bc9606ac..118cd82be12 100644 --- a/homeassistant/components/igd/__init__.py +++ b/homeassistant/components/igd/__init__.py @@ -6,7 +6,6 @@ https://home-assistant.io/components/igd/ """ import asyncio -from ipaddress import IPv4Address from ipaddress import ip_address import aiohttp @@ -17,26 +16,24 @@ from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers import config_validation as cv from homeassistant.helpers import discovery -from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.typing import ConfigType, HomeAssistantType -from homeassistant.util import get_local_ip +from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import HomeAssistantType from homeassistant.components.discovery import DOMAIN as DISCOVERY_DOMAIN from .const import ( CONF_ENABLE_PORT_MAPPING, CONF_ENABLE_SENSORS, - CONF_UDN, CONF_SSDP_DESCRIPTION + CONF_HASS, CONF_LOCAL_IP, CONF_PORTS, + CONF_UDN, CONF_SSDP_DESCRIPTION, ) from .const import DOMAIN from .const import LOGGER as _LOGGER from .config_flow import ensure_domain_data +from .device import Device REQUIREMENTS = ['async-upnp-client==0.12.4'] DEPENDENCIES = ['http'] -CONF_LOCAL_IP = 'local_ip' -CONF_PORTS = 'ports' - NOTIFICATION_ID = 'igd_notification' NOTIFICATION_TITLE = 'UPnP/IGD Setup' @@ -45,90 +42,16 @@ CONFIG_SCHEMA = vol.Schema({ vol.Optional(CONF_ENABLE_PORT_MAPPING, default=False): cv.boolean, vol.Optional(CONF_ENABLE_SENSORS, default=True): cv.boolean, vol.Optional(CONF_LOCAL_IP): vol.All(ip_address, cv.string), + vol.Optional(CONF_PORTS): + vol.Schema({vol.Any(CONF_HASS, cv.positive_int): cv.positive_int}) }), }, extra=vol.ALLOW_EXTRA) -async def _async_create_igd_device(hass: HomeAssistantType, - ssdp_description: str): - """Create IGD device.""" - # build requester - from async_upnp_client.aiohttp import AiohttpSessionRequester - session = async_get_clientsession(hass) - requester = AiohttpSessionRequester(session, True) - - # create upnp device - from async_upnp_client import UpnpFactory - factory = UpnpFactory(requester, disable_state_variable_validation=True) - try: - upnp_device = await factory.async_create_device(ssdp_description) - except (asyncio.TimeoutError, aiohttp.ClientError): - raise PlatformNotReady() - - # wrap with IgdDevice - from async_upnp_client.igd import IgdDevice - igd_device = IgdDevice(upnp_device, None) - return igd_device - - -def _store_device(hass: HomeAssistantType, udn, igd_device): - """Store an igd_device by udn.""" - if igd_device is not None: - hass.data[DOMAIN]['devices'][udn] = igd_device - elif udn in hass.data[DOMAIN]['devices']: - del hass.data[DOMAIN]['devices'][udn] - - -def _get_device(hass: HomeAssistantType, udn): - """Get an igd_device by udn.""" - return hass.data[DOMAIN]['devices'].get(udn) - - -async def _async_add_port_mapping(hass: HomeAssistantType, - igd_device, - local_ip=None): - """Create a port mapping.""" - # determine local ip, ensure sane IP - if local_ip is None: - local_ip = get_local_ip() - - if local_ip == '127.0.0.1': - _LOGGER.warning('Could not create port mapping, our IP is 127.0.0.1') - return - local_ip = IPv4Address(local_ip) - - # create port mapping - from async_upnp_client import UpnpError - port = hass.http.server_port - _LOGGER.debug('Creating port mapping %s:%s:%s (TCP)', port, local_ip, port) - try: - await igd_device.async_add_port_mapping(remote_host=None, - external_port=port, - protocol='TCP', - internal_port=port, - internal_client=local_ip, - enabled=True, - description="Home Assistant", - lease_duration=None) - except (asyncio.TimeoutError, aiohttp.ClientError, UpnpError): - _LOGGER.warning('Could not add port mapping') - - -async def _async_delete_port_mapping(hass: HomeAssistantType, igd_device): - """Remove a port mapping.""" - from async_upnp_client import UpnpError - port = hass.http.server_port - try: - await igd_device.async_delete_port_mapping(remote_host=None, - external_port=port, - protocol='TCP') - except (asyncio.TimeoutError, aiohttp.ClientError, UpnpError): - _LOGGER.warning('Could not delete port mapping') - - # config async def async_setup(hass: HomeAssistantType, config: ConfigType): """Register a port mapping for Home Assistant via UPnP.""" + _LOGGER.debug('async_setup: %s', config.get(DOMAIN)) ensure_domain_data(hass) # ensure sane config @@ -143,12 +66,22 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType): if CONF_LOCAL_IP in igd_config: hass.data[DOMAIN]['local_ip'] = igd_config[CONF_LOCAL_IP] + # determine ports + ports = {} + if CONF_PORTS in igd_config: + ports = igd_config[CONF_PORTS] + + if CONF_HASS in ports: + internal_port = hass.http.server_port + ports[internal_port] = ports[CONF_HASS] + del ports[CONF_HASS] + hass.data[DOMAIN]['auto_config'] = { 'active': True, 'port_forward': igd_config[CONF_ENABLE_PORT_MAPPING], + 'ports': ports, 'sensors': igd_config[CONF_ENABLE_SENSORS], } - _LOGGER.debug('Enabled auto_config: %s', hass.data[DOMAIN]['auto_config']) return True @@ -157,27 +90,31 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType): async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigEntry): """Set up a bridge from a config entry.""" + _LOGGER.debug('async_setup_entry: %s', config_entry.data) ensure_domain_data(hass) data = config_entry.data # build IGD device ssdp_description = data[CONF_SSDP_DESCRIPTION] try: - igd_device = await _async_create_igd_device(hass, ssdp_description) + device = await Device.async_create_device(hass, ssdp_description) except (asyncio.TimeoutError, aiohttp.ClientError): raise PlatformNotReady() - _store_device(hass, igd_device.udn, igd_device) + hass.data[DOMAIN]['devices'][device.udn] = device # port mapping if data.get(CONF_ENABLE_PORT_MAPPING): local_ip = hass.data[DOMAIN].get('local_ip') - await _async_add_port_mapping(hass, igd_device, local_ip=local_ip) + ports = hass.data[DOMAIN]['auto_config']['ports'] + _LOGGER.debug('Enabling port mappings: %s', ports) + await device.async_add_port_mappings(ports, local_ip=local_ip) # sensors if data.get(CONF_ENABLE_SENSORS): + _LOGGER.debug('Enabling sensors') discovery_info = { - 'udn': data[CONF_UDN], + 'udn': device.udn, } hass_config = config_entry.data hass.async_create_task(discovery.async_load_platform( @@ -194,22 +131,25 @@ async def async_setup_entry(hass: HomeAssistantType, async def async_unload_entry(hass: HomeAssistantType, config_entry: ConfigEntry): """Unload a config entry.""" + _LOGGER.debug('async_unload_entry: %s', config_entry.data) data = config_entry.data udn = data[CONF_UDN] - igd_device = _get_device(hass, udn) - if igd_device is None: + if udn not in hass.data[DOMAIN]['devices']: return True + device = hass.data[DOMAIN]['devices'][udn] # port mapping if data.get(CONF_ENABLE_PORT_MAPPING): - await _async_delete_port_mapping(hass, igd_device) + _LOGGER.debug('Deleting port mappings') + await device.async_delete_port_mappings() # sensors for sensor in hass.data[DOMAIN]['sensors'].get(udn, []): + _LOGGER.debug('Deleting sensor: %s', sensor) await sensor.async_remove() # clear stored device - _store_device(hass, udn, None) + del hass.data[DOMAIN]['devices'][udn] return True diff --git a/homeassistant/components/igd/config_flow.py b/homeassistant/components/igd/config_flow.py index 6f0cec12b5b..7af6c683ed9 100644 --- a/homeassistant/components/igd/config_flow.py +++ b/homeassistant/components/igd/config_flow.py @@ -21,6 +21,7 @@ def ensure_domain_data(hass): 'active': False, 'port_forward': False, 'sensors': False, + 'ports': {}, }) diff --git a/homeassistant/components/igd/const.py b/homeassistant/components/igd/const.py index 47ff9f11074..8ba774447da 100644 --- a/homeassistant/components/igd/const.py +++ b/homeassistant/components/igd/const.py @@ -2,9 +2,12 @@ import logging -DOMAIN = 'igd' -LOGGER = logging.getLogger('homeassistant.components.igd') CONF_ENABLE_PORT_MAPPING = 'port_forward' CONF_ENABLE_SENSORS = 'sensors' -CONF_UDN = 'udn' +CONF_HASS = 'hass' +CONF_LOCAL_IP = 'local_ip' +CONF_PORTS = 'ports' CONF_SSDP_DESCRIPTION = 'ssdp_description' +CONF_UDN = 'udn' +DOMAIN = 'igd' +LOGGER = logging.getLogger('homeassistant.components.igd') diff --git a/homeassistant/components/igd/device.py b/homeassistant/components/igd/device.py new file mode 100644 index 00000000000..af16623e4b2 --- /dev/null +++ b/homeassistant/components/igd/device.py @@ -0,0 +1,131 @@ +"""Hass representation of an IGD.""" + +import asyncio +from ipaddress import IPv4Address + +import aiohttp + +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.util import get_local_ip + +from .const import LOGGER as _LOGGER + + +class Device: + """Hass representation of an IGD.""" + + def __init__(self, igd_device): + """Initializer.""" + self._igd_device = igd_device + self._mapped_ports = [] + + @classmethod + async def async_create_device(cls, + hass: HomeAssistantType, + ssdp_description: str): + """Create IGD device.""" + # build async_upnp_client requester + from async_upnp_client.aiohttp import AiohttpSessionRequester + session = async_get_clientsession(hass) + requester = AiohttpSessionRequester(session, True) + + # create async_upnp_client device + from async_upnp_client import UpnpFactory + factory = UpnpFactory(requester, + disable_state_variable_validation=True) + upnp_device = await factory.async_create_device(ssdp_description) + + # wrap with async_upnp_client IgdDevice + from async_upnp_client.igd import IgdDevice + igd_device = IgdDevice(upnp_device, None) + + return Device(igd_device) + + @property + def udn(self): + """Get the UDN.""" + return self._igd_device.udn + + @property + def name(self): + """Get the name.""" + return self._igd_device.name + + async def async_add_port_mappings(self, ports, local_ip=None): + """Add port mappings.""" + # determine local ip, ensure sane IP + if local_ip is None: + local_ip = get_local_ip() + + if local_ip == '127.0.0.1': + _LOGGER.error( + 'Could not create port mapping, our IP is 127.0.0.1') + local_ip = IPv4Address(local_ip) + + # create port mappings + for external_port, internal_port in ports.items(): + await self._async_add_port_mapping(external_port, + local_ip, + internal_port) + self._mapped_ports.append(external_port) + + async def _async_add_port_mapping(self, + external_port, + local_ip, + internal_port): + """Add a port mapping.""" + # create port mapping + from async_upnp_client import UpnpError + _LOGGER.info('Creating port mapping %s:%s:%s (TCP)', + external_port, local_ip, internal_port) + try: + await self._igd_device.async_add_port_mapping( + remote_host=None, + external_port=external_port, + protocol='TCP', + internal_port=internal_port, + internal_client=local_ip, + enabled=True, + description="Home Assistant", + lease_duration=None) + + self._mapped_ports.append(external_port) + except (asyncio.TimeoutError, aiohttp.ClientError, UpnpError): + _LOGGER.error('Could not add port mapping: %s:%s:%s', + external_port, local_ip, internal_port) + + async def async_delete_port_mappings(self): + """Remove a port mapping.""" + for port in self._mapped_ports: + await self._async_delete_port_mapping(port) + + async def _async_delete_port_mapping(self, external_port): + """Remove a port mapping.""" + from async_upnp_client import UpnpError + try: + await self._igd_device.async_delete_port_mapping( + remote_host=None, + external_port=external_port, + protocol='TCP') + + self._mapped_ports.remove(external_port) + except (asyncio.TimeoutError, aiohttp.ClientError, UpnpError): + _LOGGER.error('Could not delete port mapping') + + async def async_get_total_bytes_received(self): + """Get total bytes received.""" + return await self._igd_device.async_get_total_bytes_received() + + async def async_get_total_bytes_sent(self): + """Get total bytes sent.""" + return await self._igd_device.async_get_total_bytes_sent() + + async def async_get_total_packets_received(self): + """Get total packets received.""" + # pylint: disable=invalid-name + return await self._igd_device.async_get_total_packets_received() + + async def async_get_total_packets_sent(self): + """Get total packets sent.""" + return await self._igd_device.async_get_total_packets_sent() diff --git a/homeassistant/components/sensor/igd.py b/homeassistant/components/sensor/igd.py index 908e465aedb..ff93af89f34 100644 --- a/homeassistant/components/sensor/igd.py +++ b/homeassistant/components/sensor/igd.py @@ -14,7 +14,7 @@ from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) -DEPENDENCIES = ['igd', 'history'] +DEPENDENCIES = ['igd'] BYTES_RECEIVED = 'bytes_received' BYTES_SENT = 'bytes_sent' @@ -52,18 +52,18 @@ async def async_setup_platform(hass, config, async_add_devices, return udn = discovery_info['udn'] - igd_device = hass.data[DOMAIN]['devices'][udn] + device = hass.data[DOMAIN]['devices'][udn] # raw sensors + per-second sensors sensors = [ - RawIGDSensor(igd_device, name, sensor_type) + RawIGDSensor(device, name, sensor_type) for name, sensor_type in SENSOR_TYPES.items() ] sensors += [ - KBytePerSecondIGDSensor(igd_device, IN), - KBytePerSecondIGDSensor(igd_device, OUT), - PacketsPerSecondIGDSensor(igd_device, IN), - PacketsPerSecondIGDSensor(igd_device, OUT), + KBytePerSecondIGDSensor(device, IN), + KBytePerSecondIGDSensor(device, OUT), + PacketsPerSecondIGDSensor(device, IN), + PacketsPerSecondIGDSensor(device, OUT), ] hass.data[DOMAIN]['sensors'][udn] = sensors async_add_devices(sensors, True) @@ -108,7 +108,6 @@ class RawIGDSensor(Entity): async def async_update(self): """Get the latest information from the IGD.""" - _LOGGER.debug('%s: async_update', self) if self._type_name == BYTES_RECEIVED: self._state = await self._device.async_get_total_bytes_received() elif self._type_name == BYTES_SENT: @@ -171,7 +170,6 @@ class PerSecondIGDSensor(Entity): async def async_update(self): """Get the latest information from the IGD.""" - _LOGGER.debug('%s: async_update', self) new_value = await self._async_fetch_value() if self._last_value is None: diff --git a/tests/components/igd/test_init.py b/tests/components/igd/test_init.py index cd3f5324bb4..336935710d8 100644 --- a/tests/components/igd/test_init.py +++ b/tests/components/igd/test_init.py @@ -1,15 +1,51 @@ """Test IGD setup process.""" +from ipaddress import ip_address from unittest.mock import patch, MagicMock from homeassistant.setup import async_setup_component from homeassistant.components import igd +from homeassistant.components.igd.device import Device from homeassistant.const import EVENT_HOMEASSISTANT_STOP from tests.common import MockConfigEntry from tests.common import mock_coro +class MockDevice(Device): + """Mock device for Device.""" + + def __init__(self, udn): + """Initializer.""" + super().__init__(None) + self._udn = udn + self.added_port_mappings = [] + self.removed_port_mappings = [] + + @classmethod + async def async_create_device(cls, hass, ssdp_description): + """Return self.""" + return cls() + + @property + def udn(self): + """Get the UDN.""" + return self._udn + + async def _async_add_port_mapping(self, + external_port, + local_ip, + internal_port): + """Add a port mapping.""" + entry = [external_port, local_ip, internal_port] + self.added_port_mappings.append(entry) + + async def _async_delete_port_mapping(self, external_port): + """Remove a port mapping.""" + entry = external_port + self.removed_port_mappings.append(entry) + + async def test_async_setup_no_auto_config(hass): """Test async_setup.""" # setup component, enable auto_config @@ -18,6 +54,7 @@ async def test_async_setup_no_auto_config(hass): assert hass.data[igd.DOMAIN]['auto_config'] == { 'active': False, 'port_forward': False, + 'ports': {}, 'sensors': False, } @@ -30,6 +67,7 @@ async def test_async_setup_auto_config(hass): assert hass.data[igd.DOMAIN]['auto_config'] == { 'active': True, 'port_forward': False, + 'ports': {}, 'sensors': True, } @@ -38,12 +76,13 @@ async def test_async_setup_auto_config_port_forward(hass): """Test async_setup.""" # setup component, enable auto_config await async_setup_component(hass, 'igd', { - 'igd': {'port_forward': True}, + 'igd': {'port_forward': True, 'ports': {8123: 8123}}, 'discovery': {}}) assert hass.data[igd.DOMAIN]['auto_config'] == { 'active': True, 'port_forward': True, + 'ports': {8123: 8123}, 'sensors': True, } @@ -58,6 +97,7 @@ async def test_async_setup_auto_config_no_sensors(hass): assert hass.data[igd.DOMAIN]['auto_config'] == { 'active': True, 'port_forward': False, + 'ports': {}, 'sensors': False, } @@ -75,27 +115,30 @@ async def test_async_setup_entry_default(hass): # ensure hass.http is available await async_setup_component(hass, 'igd') - # mock async_upnp_client.igd.IgdDevice - mock_igd_device = MagicMock() - mock_igd_device.udn = udn - mock_igd_device.async_add_port_mapping.return_value = mock_coro() - mock_igd_device.async_delete_port_mapping.return_value = mock_coro() - with patch.object(igd, '_async_create_igd_device') as mock_create_device: + # mock homeassistant.components.igd.device.Device + mock_device = MagicMock() + mock_device.udn = udn + mock_device.async_add_port_mappings.return_value = mock_coro() + mock_device.async_delete_port_mappings.return_value = mock_coro() + with patch.object(Device, 'async_create_device') as mock_create_device: mock_create_device.return_value = mock_coro( - return_value=mock_igd_device) - with patch('homeassistant.components.igd.get_local_ip', + return_value=mock_device) + with patch('homeassistant.components.igd.device.get_local_ip', return_value='192.168.1.10'): assert await igd.async_setup_entry(hass, entry) is True # ensure device is stored/used - assert hass.data[igd.DOMAIN]['devices'][udn] == mock_igd_device + assert hass.data[igd.DOMAIN]['devices'][udn] == mock_device - hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) - await hass.async_block_till_done() + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + # ensure cleaned up assert udn not in hass.data[igd.DOMAIN]['devices'] - assert len(mock_igd_device.async_add_port_mapping.mock_calls) == 0 - assert len(mock_igd_device.async_delete_port_mapping.mock_calls) == 0 + + # ensure no port-mapping-methods called + assert len(mock_device.async_add_port_mappings.mock_calls) == 0 + assert len(mock_device.async_delete_port_mappings.mock_calls) == 0 async def test_async_setup_entry_port_forward(hass): @@ -109,25 +152,30 @@ async def test_async_setup_entry_port_forward(hass): }) # ensure hass.http is available - await async_setup_component(hass, 'igd') + await async_setup_component(hass, 'igd', { + 'igd': {'port_forward': True, 'ports': {8123: 8123}}, + 'discovery': {}}) - mock_igd_device = MagicMock() - mock_igd_device.udn = udn - mock_igd_device.async_add_port_mapping.return_value = mock_coro() - mock_igd_device.async_delete_port_mapping.return_value = mock_coro() - with patch.object(igd, '_async_create_igd_device') as mock_create_device: - mock_create_device.return_value = mock_coro( - return_value=mock_igd_device) - with patch('homeassistant.components.igd.get_local_ip', + mock_device = MockDevice(udn) + with patch.object(Device, 'async_create_device') as mock_create_device: + mock_create_device.return_value = mock_coro(return_value=mock_device) + with patch('homeassistant.components.igd.device.get_local_ip', return_value='192.168.1.10'): assert await igd.async_setup_entry(hass, entry) is True # ensure device is stored/used - assert hass.data[igd.DOMAIN]['devices'][udn] == mock_igd_device + assert hass.data[igd.DOMAIN]['devices'][udn] == mock_device - hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) - await hass.async_block_till_done() + # ensure add-port-mapping-methods called + assert mock_device.added_port_mappings == [ + [8123, ip_address('192.168.1.10'), 8123] + ] + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + + # ensure cleaned up assert udn not in hass.data[igd.DOMAIN]['devices'] - assert len(mock_igd_device.async_add_port_mapping.mock_calls) > 0 - assert len(mock_igd_device.async_delete_port_mapping.mock_calls) > 0 + + # ensure delete-port-mapping-methods called + assert mock_device.removed_port_mappings == [8123] From ca7911ec470a51254ec077d64779504ace735791 Mon Sep 17 00:00:00 2001 From: Steven Looman Date: Fri, 14 Sep 2018 21:30:08 +0200 Subject: [PATCH 014/247] Set ports (hass -> hass) when no ports have been specified --- homeassistant/components/igd/__init__.py | 27 +++++++++++++++------ homeassistant/components/igd/config_flow.py | 2 +- tests/components/igd/test_init.py | 21 ++++++++++------ 3 files changed, 35 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/igd/__init__.py b/homeassistant/components/igd/__init__.py index 118cd82be12..1940430bdc1 100644 --- a/homeassistant/components/igd/__init__.py +++ b/homeassistant/components/igd/__init__.py @@ -43,11 +43,24 @@ CONFIG_SCHEMA = vol.Schema({ vol.Optional(CONF_ENABLE_SENSORS, default=True): cv.boolean, vol.Optional(CONF_LOCAL_IP): vol.All(ip_address, cv.string), vol.Optional(CONF_PORTS): - vol.Schema({vol.Any(CONF_HASS, cv.positive_int): cv.positive_int}) + vol.Schema({ + vol.Any(CONF_HASS, cv.positive_int): + vol.Any(CONF_HASS, cv.positive_int) + }) }), }, extra=vol.ALLOW_EXTRA) +def _substitute_hass_ports(ports, hass_port): + # substitute 'hass' for hass_port, both sides + if CONF_HASS in ports: + ports[hass_port] = ports[CONF_HASS] + del ports[CONF_HASS] + for port in ports: + if ports[port] == CONF_HASS: + ports[port] = hass_port + + # config async def async_setup(hass: HomeAssistantType, config: ConfigType): """Register a port mapping for Home Assistant via UPnP.""" @@ -62,20 +75,17 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType): _LOGGER.warning('IGD needs discovery, please enable it') return False + # overridden local ip igd_config = config[DOMAIN] if CONF_LOCAL_IP in igd_config: hass.data[DOMAIN]['local_ip'] = igd_config[CONF_LOCAL_IP] # determine ports - ports = {} + ports = {CONF_HASS: CONF_HASS} # default, port_forward disabled by default if CONF_PORTS in igd_config: + # copy from config ports = igd_config[CONF_PORTS] - if CONF_HASS in ports: - internal_port = hass.http.server_port - ports[internal_port] = ports[CONF_HASS] - del ports[CONF_HASS] - hass.data[DOMAIN]['auto_config'] = { 'active': True, 'port_forward': igd_config[CONF_ENABLE_PORT_MAPPING], @@ -108,6 +118,9 @@ async def async_setup_entry(hass: HomeAssistantType, local_ip = hass.data[DOMAIN].get('local_ip') ports = hass.data[DOMAIN]['auto_config']['ports'] _LOGGER.debug('Enabling port mappings: %s', ports) + + hass_port = hass.http.server_port + _substitute_hass_ports(ports, hass_port) await device.async_add_port_mappings(ports, local_ip=local_ip) # sensors diff --git a/homeassistant/components/igd/config_flow.py b/homeassistant/components/igd/config_flow.py index 7af6c683ed9..1ad4b2a41de 100644 --- a/homeassistant/components/igd/config_flow.py +++ b/homeassistant/components/igd/config_flow.py @@ -21,7 +21,7 @@ def ensure_domain_data(hass): 'active': False, 'port_forward': False, 'sensors': False, - 'ports': {}, + 'ports': {'hass': 'hass'}, }) diff --git a/tests/components/igd/test_init.py b/tests/components/igd/test_init.py index 336935710d8..048313affb7 100644 --- a/tests/components/igd/test_init.py +++ b/tests/components/igd/test_init.py @@ -54,7 +54,7 @@ async def test_async_setup_no_auto_config(hass): assert hass.data[igd.DOMAIN]['auto_config'] == { 'active': False, 'port_forward': False, - 'ports': {}, + 'ports': {'hass': 'hass'}, 'sensors': False, } @@ -67,7 +67,7 @@ async def test_async_setup_auto_config(hass): assert hass.data[igd.DOMAIN]['auto_config'] == { 'active': True, 'port_forward': False, - 'ports': {}, + 'ports': {'hass': 'hass'}, 'sensors': True, } @@ -76,13 +76,16 @@ async def test_async_setup_auto_config_port_forward(hass): """Test async_setup.""" # setup component, enable auto_config await async_setup_component(hass, 'igd', { - 'igd': {'port_forward': True, 'ports': {8123: 8123}}, + 'igd': { + 'port_forward': True, + 'ports': {'hass': 'hass'}, + }, 'discovery': {}}) assert hass.data[igd.DOMAIN]['auto_config'] == { 'active': True, 'port_forward': True, - 'ports': {8123: 8123}, + 'ports': {'hass': 'hass'}, 'sensors': True, } @@ -97,7 +100,7 @@ async def test_async_setup_auto_config_no_sensors(hass): assert hass.data[igd.DOMAIN]['auto_config'] == { 'active': True, 'port_forward': False, - 'ports': {}, + 'ports': {'hass': 'hass'}, 'sensors': False, } @@ -153,8 +156,12 @@ async def test_async_setup_entry_port_forward(hass): # ensure hass.http is available await async_setup_component(hass, 'igd', { - 'igd': {'port_forward': True, 'ports': {8123: 8123}}, - 'discovery': {}}) + 'igd': { + 'port_forward': True, + 'ports': {'hass': 'hass'}, + }, + 'discovery': {}, + }) mock_device = MockDevice(udn) with patch.object(Device, 'async_create_device') as mock_create_device: From 6588dceadded82ac5ad21e5182a1f6b5f861fbab Mon Sep 17 00:00:00 2001 From: Steven Looman Date: Mon, 17 Sep 2018 21:34:39 +0200 Subject: [PATCH 015/247] Fix uppercase Fix translation --- homeassistant/components/igd/.translations/en.json | 2 +- homeassistant/components/igd/.translations/nl.json | 4 ++-- homeassistant/components/igd/strings.json | 7 +++---- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/igd/.translations/en.json b/homeassistant/components/igd/.translations/en.json index 3466b6cfa93..d6117f0052f 100644 --- a/homeassistant/components/igd/.translations/en.json +++ b/homeassistant/components/igd/.translations/en.json @@ -19,7 +19,7 @@ "abort": { "no_devices_discovered": "No IGDs discovered", "already_configured": "IGD is already configured", - "no_sensors_or_port_forward": "Enable at least sensors or Port forward", + "no_sensors_or_port_forward": "Enable at least sensors or port forward", "no_igds": "No IGDs discovered" } } diff --git a/homeassistant/components/igd/.translations/nl.json b/homeassistant/components/igd/.translations/nl.json index 5a42f6c3bea..9fdd831f74c 100644 --- a/homeassistant/components/igd/.translations/nl.json +++ b/homeassistant/components/igd/.translations/nl.json @@ -10,7 +10,7 @@ "data":{ "igd": "IGD", "sensors": "Verkeer sensors toevoegen", - "port_forward": "Maak port-forward voor Home Assistant" + "port_forward": "Maak port forward voor Home Assistant" } } }, @@ -19,7 +19,7 @@ "abort": { "no_devices_discovered": "Geen IGDs gevonden", "already_configured": "IGD is reeds geconfigureerd", - "no_sensors_or_port_forward": "Kies ten minste sensors of Port forward", + "no_sensors_or_port_forward": "Kies ten minste sensors of port forward", "no_igds": "Geen IGDs gevonden" } } diff --git a/homeassistant/components/igd/strings.json b/homeassistant/components/igd/strings.json index 3466b6cfa93..5ab50b3a7d0 100644 --- a/homeassistant/components/igd/strings.json +++ b/homeassistant/components/igd/strings.json @@ -17,10 +17,9 @@ "error": { }, "abort": { - "no_devices_discovered": "No IGDs discovered", - "already_configured": "IGD is already configured", - "no_sensors_or_port_forward": "Enable at least sensors or Port forward", - "no_igds": "No IGDs discovered" + "no_devices_discovered": "No UPnP/IGDs discovered", + "already_configured": "UPnP/IGD is already configured", + "no_sensors_or_port_forward": "Enable at least sensors or port forward" } } } From c19665fed41ecc2a233a59bc61b351f863f2fad1 Mon Sep 17 00:00:00 2001 From: Steven Looman Date: Mon, 17 Sep 2018 22:08:09 +0200 Subject: [PATCH 016/247] Rename igd to upnp --- homeassistant/components/discovery.py | 2 +- .../components/sensor/{igd.py => upnp.py} | 34 ++++++------ .../{igd => upnp}/.translations/en.json | 15 +++--- .../{igd => upnp}/.translations/nl.json | 15 +++--- .../components/{igd => upnp}/__init__.py | 23 ++++---- .../components/{igd => upnp}/config_flow.py | 28 +++++----- .../components/{igd => upnp}/const.py | 4 +- .../components/{igd => upnp}/device.py | 7 ++- .../components/{igd => upnp}/strings.json | 8 +-- homeassistant/config_entries.py | 2 +- tests/components/{igd => upnp}/__init__.py | 0 .../{igd => upnp}/test_config_flow.py | 42 +++++++-------- tests/components/{igd => upnp}/test_init.py | 54 +++++++++---------- 13 files changed, 115 insertions(+), 119 deletions(-) rename homeassistant/components/sensor/{igd.py => upnp.py} (88%) rename homeassistant/components/{igd => upnp}/.translations/en.json (55%) rename homeassistant/components/{igd => upnp}/.translations/nl.json (55%) rename homeassistant/components/{igd => upnp}/__init__.py (90%) rename homeassistant/components/{igd => upnp}/config_flow.py (86%) rename homeassistant/components/{igd => upnp}/const.py (77%) rename homeassistant/components/{igd => upnp}/device.py (97%) rename homeassistant/components/{igd => upnp}/strings.json (77%) rename tests/components/{igd => upnp}/__init__.py (100%) rename tests/components/{igd => upnp}/test_config_flow.py (87%) rename tests/components/{igd => upnp}/test_init.py (75%) diff --git a/homeassistant/components/discovery.py b/homeassistant/components/discovery.py index 8009f01222b..092e8b7b578 100644 --- a/homeassistant/components/discovery.py +++ b/homeassistant/components/discovery.py @@ -49,7 +49,7 @@ CONFIG_ENTRY_HANDLERS = { 'google_cast': 'cast', SERVICE_HUE: 'hue', 'sonos': 'sonos', - 'igd': 'igd', + 'upnp': 'igd', } SERVICE_HANDLERS = { diff --git a/homeassistant/components/sensor/igd.py b/homeassistant/components/sensor/upnp.py similarity index 88% rename from homeassistant/components/sensor/igd.py rename to homeassistant/components/sensor/upnp.py index ff93af89f34..3c3745145a4 100644 --- a/homeassistant/components/sensor/igd.py +++ b/homeassistant/components/sensor/upnp.py @@ -1,20 +1,20 @@ """ -Support for IGD Sensors. +Support for UPnP/IGD Sensors. For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.igd/ +https://home-assistant.io/components/sensor.upnp/ """ # pylint: disable=invalid-name from datetime import datetime import logging -from homeassistant.components.igd import DOMAIN +from homeassistant.components.upnp import DOMAIN from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) -DEPENDENCIES = ['igd'] +DEPENDENCIES = ['upnp'] BYTES_RECEIVED = 'bytes_received' BYTES_SENT = 'bytes_sent' @@ -47,7 +47,7 @@ KBYTE = 1024 async def async_setup_platform(hass, config, async_add_devices, discovery_info=None): - """Set up the IGD sensors.""" + """Set up the UPnP/IGD sensors.""" if discovery_info is None: return @@ -56,25 +56,25 @@ async def async_setup_platform(hass, config, async_add_devices, # raw sensors + per-second sensors sensors = [ - RawIGDSensor(device, name, sensor_type) + RawUPnPIGDSensor(device, name, sensor_type) for name, sensor_type in SENSOR_TYPES.items() ] sensors += [ - KBytePerSecondIGDSensor(device, IN), - KBytePerSecondIGDSensor(device, OUT), - PacketsPerSecondIGDSensor(device, IN), - PacketsPerSecondIGDSensor(device, OUT), + KBytePerSecondUPnPIGDSensor(device, IN), + KBytePerSecondUPnPIGDSensor(device, OUT), + PacketsPerSecondUPnPIGDSensor(device, IN), + PacketsPerSecondUPnPIGDSensor(device, OUT), ] hass.data[DOMAIN]['sensors'][udn] = sensors async_add_devices(sensors, True) return True -class RawIGDSensor(Entity): - """Representation of a UPnP IGD sensor.""" +class RawUPnPIGDSensor(Entity): + """Representation of a UPnP/IGD sensor.""" def __init__(self, device, sensor_type_name, sensor_type): - """Initialize the IGD sensor.""" + """Initialize the UPnP/IGD sensor.""" self._device = device self._type_name = sensor_type_name self._type = sensor_type @@ -118,7 +118,7 @@ class RawIGDSensor(Entity): self._state = await self._device.async_get_total_packets_sent() -class PerSecondIGDSensor(Entity): +class PerSecondUPnPIGDSensor(Entity): """Abstract representation of a X Sent/Received per second sensor.""" def __init__(self, device, direction): @@ -169,7 +169,7 @@ class PerSecondIGDSensor(Entity): return new_value < self._last_value async def async_update(self): - """Get the latest information from the IGD.""" + """Get the latest information from the UPnP/IGD.""" new_value = await self._async_fetch_value() if self._last_value is None: @@ -189,7 +189,7 @@ class PerSecondIGDSensor(Entity): self._last_update_time = now -class KBytePerSecondIGDSensor(PerSecondIGDSensor): +class KBytePerSecondUPnPIGDSensor(PerSecondUPnPIGDSensor): """Representation of a KBytes Sent/Received per second sensor.""" @property @@ -213,7 +213,7 @@ class KBytePerSecondIGDSensor(PerSecondIGDSensor): return format(float(self._state / KBYTE), '.1f') -class PacketsPerSecondIGDSensor(PerSecondIGDSensor): +class PacketsPerSecondUPnPIGDSensor(PerSecondUPnPIGDSensor): """Representation of a Packets Sent/Received per second sensor.""" @property diff --git a/homeassistant/components/igd/.translations/en.json b/homeassistant/components/upnp/.translations/en.json similarity index 55% rename from homeassistant/components/igd/.translations/en.json rename to homeassistant/components/upnp/.translations/en.json index d6117f0052f..829631254ad 100644 --- a/homeassistant/components/igd/.translations/en.json +++ b/homeassistant/components/upnp/.translations/en.json @@ -1,14 +1,14 @@ { "config": { - "title": "IGD", + "title": "UPnP/IGD", "step": { "init": { - "title": "IGD" + "title": "UPnP/IGD" }, "user": { - "title": "Configuration options for the IGD", + "title": "Configuration options for the UPnP/IGD", "data":{ - "igd": "IGD", + "igd": "UPnP/IGD", "sensors": "Add traffic sensors", "port_forward": "Enable port forward for Home Assistant" } @@ -17,10 +17,9 @@ "error": { }, "abort": { - "no_devices_discovered": "No IGDs discovered", - "already_configured": "IGD is already configured", - "no_sensors_or_port_forward": "Enable at least sensors or port forward", - "no_igds": "No IGDs discovered" + "no_devices_discovered": "No UPnP/IGDs discovered", + "already_configured": "UPnP/IGD is already configured", + "no_sensors_or_port_forward": "Enable at least sensors or port forward" } } } diff --git a/homeassistant/components/igd/.translations/nl.json b/homeassistant/components/upnp/.translations/nl.json similarity index 55% rename from homeassistant/components/igd/.translations/nl.json rename to homeassistant/components/upnp/.translations/nl.json index 9fdd831f74c..02d8fcc0913 100644 --- a/homeassistant/components/igd/.translations/nl.json +++ b/homeassistant/components/upnp/.translations/nl.json @@ -1,14 +1,14 @@ { "config": { - "title": "IGD", + "title": "UPnP/IGD", "step": { "init": { - "title": "IGD" + "title": "UPnP/IGD" }, "user": { - "title": "Extra configuratie options voor IGD", + "title": "Extra configuratie options voor UPnP/IGD", "data":{ - "igd": "IGD", + "igd": "UPnP/IGD", "sensors": "Verkeer sensors toevoegen", "port_forward": "Maak port forward voor Home Assistant" } @@ -17,10 +17,9 @@ "error": { }, "abort": { - "no_devices_discovered": "Geen IGDs gevonden", - "already_configured": "IGD is reeds geconfigureerd", - "no_sensors_or_port_forward": "Kies ten minste sensors of port forward", - "no_igds": "Geen IGDs gevonden" + "no_devices_discovered": "Geen UPnP/IGDs gevonden", + "already_configured": "UPnP/IGD is reeds geconfigureerd", + "no_sensors_or_port_forward": "Kies ten minste sensors of port forward" } } } diff --git a/homeassistant/components/igd/__init__.py b/homeassistant/components/upnp/__init__.py similarity index 90% rename from homeassistant/components/igd/__init__.py rename to homeassistant/components/upnp/__init__.py index 1940430bdc1..a39ae210131 100644 --- a/homeassistant/components/igd/__init__.py +++ b/homeassistant/components/upnp/__init__.py @@ -2,9 +2,8 @@ Will open a port in your router for Home Assistant and provide statistics. For more details about this component, please refer to the documentation at -https://home-assistant.io/components/igd/ +https://home-assistant.io/components/upnp/ """ - import asyncio from ipaddress import ip_address @@ -34,7 +33,7 @@ from .device import Device REQUIREMENTS = ['async-upnp-client==0.12.4'] DEPENDENCIES = ['http'] -NOTIFICATION_ID = 'igd_notification' +NOTIFICATION_ID = 'upnp_notification' NOTIFICATION_TITLE = 'UPnP/IGD Setup' CONFIG_SCHEMA = vol.Schema({ @@ -72,25 +71,25 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType): return True if DISCOVERY_DOMAIN not in config: - _LOGGER.warning('IGD needs discovery, please enable it') + _LOGGER.warning('UPNP needs discovery, please enable it') return False # overridden local ip - igd_config = config[DOMAIN] - if CONF_LOCAL_IP in igd_config: - hass.data[DOMAIN]['local_ip'] = igd_config[CONF_LOCAL_IP] + upnp_config = config[DOMAIN] + if CONF_LOCAL_IP in upnp_config: + hass.data[DOMAIN]['local_ip'] = upnp_config[CONF_LOCAL_IP] # determine ports ports = {CONF_HASS: CONF_HASS} # default, port_forward disabled by default - if CONF_PORTS in igd_config: + if CONF_PORTS in upnp_config: # copy from config - ports = igd_config[CONF_PORTS] + ports = upnp_config[CONF_PORTS] hass.data[DOMAIN]['auto_config'] = { 'active': True, - 'port_forward': igd_config[CONF_ENABLE_PORT_MAPPING], + 'port_forward': upnp_config[CONF_ENABLE_PORT_MAPPING], 'ports': ports, - 'sensors': igd_config[CONF_ENABLE_SENSORS], + 'sensors': upnp_config[CONF_ENABLE_SENSORS], } return True @@ -104,7 +103,7 @@ async def async_setup_entry(hass: HomeAssistantType, ensure_domain_data(hass) data = config_entry.data - # build IGD device + # build UPnP/IGD device ssdp_description = data[CONF_SSDP_DESCRIPTION] try: device = await Device.async_create_device(hass, ssdp_description) diff --git a/homeassistant/components/igd/config_flow.py b/homeassistant/components/upnp/config_flow.py similarity index 86% rename from homeassistant/components/igd/config_flow.py rename to homeassistant/components/upnp/config_flow.py index 1ad4b2a41de..5c75d0e0517 100644 --- a/homeassistant/components/igd/config_flow.py +++ b/homeassistant/components/upnp/config_flow.py @@ -1,4 +1,4 @@ -"""Config flow for IGD.""" +"""Config flow for UPNP.""" import voluptuous as vol from homeassistant import config_entries @@ -26,13 +26,13 @@ def ensure_domain_data(hass): @config_entries.HANDLERS.register(DOMAIN) -class IgdFlowHandler(data_entry_flow.FlowHandler): +class UpnpFlowHandler(data_entry_flow.FlowHandler): """Handle a Hue config flow.""" VERSION = 1 @property - def _configured_igds(self): + def _configured_upnp_igds(self): """Get all configured IGDs.""" return { entry.data[CONF_UDN]: { @@ -42,7 +42,7 @@ class IgdFlowHandler(data_entry_flow.FlowHandler): } @property - def _discovered_igds(self): + def _discovered_upnp_igds(self): """Get all discovered entries.""" return self.hass.data[DOMAIN]['discovered'] @@ -57,7 +57,7 @@ class IgdFlowHandler(data_entry_flow.FlowHandler): async def async_step_discovery(self, discovery_info): """ - Handle a discovered IGD. + Handle a discovered UPnP/IGD. This flow is triggered by the discovery component. It will check if the host is already configured and delegate to the import step if not. @@ -71,7 +71,7 @@ class IgdFlowHandler(data_entry_flow.FlowHandler): # ensure not already discovered/configured udn = discovery_info['udn'] - if udn in self._configured_igds: + if udn in self._configured_upnp_igds: return self.async_abort(reason='already_configured') # auto config? @@ -98,19 +98,19 @@ class IgdFlowHandler(data_entry_flow.FlowHandler): # ensure not already configured configured_names = [ entry['friendly_name'] - for udn, entry in self._discovered_igds.items() - if udn in self._configured_igds + for udn, entry in self._discovered_upnp_igds.items() + if udn in self._configured_upnp_igds ] if user_input['name'] in configured_names: return self.async_abort(reason='already_configured') return await self._async_save_entry(user_input) - # let user choose from all discovered, non-configured, IGDs + # let user choose from all discovered, non-configured, UPnP/IGDs names = [ entry['friendly_name'] - for udn, entry in self._discovered_igds.items() - if udn not in self._configured_igds + for udn, entry in self._discovered_upnp_igds.items() + if udn not in self._configured_upnp_igds ] if not names: return self.async_abort(reason='no_devices_discovered') @@ -125,15 +125,15 @@ class IgdFlowHandler(data_entry_flow.FlowHandler): ) async def async_step_import(self, import_info): - """Import a new IGD as a config entry.""" + """Import a new UPnP/IGD as a config entry.""" return await self._async_save_entry(import_info) async def _async_save_entry(self, import_info): - """Store IGD as new entry.""" + """Store UPNP/IGD as new entry.""" # ensure we know the host name = import_info['name'] discovery_infos = [info - for info in self._discovered_igds.values() + for info in self._discovered_upnp_igds.values() if info['friendly_name'] == name] if not discovery_infos: return self.async_abort(reason='host_not_found') diff --git a/homeassistant/components/igd/const.py b/homeassistant/components/upnp/const.py similarity index 77% rename from homeassistant/components/igd/const.py rename to homeassistant/components/upnp/const.py index 8ba774447da..0728747af3d 100644 --- a/homeassistant/components/igd/const.py +++ b/homeassistant/components/upnp/const.py @@ -9,5 +9,5 @@ CONF_LOCAL_IP = 'local_ip' CONF_PORTS = 'ports' CONF_SSDP_DESCRIPTION = 'ssdp_description' CONF_UDN = 'udn' -DOMAIN = 'igd' -LOGGER = logging.getLogger('homeassistant.components.igd') +DOMAIN = 'upnp' +LOGGER = logging.getLogger('homeassistant.components.upnp') diff --git a/homeassistant/components/igd/device.py b/homeassistant/components/upnp/device.py similarity index 97% rename from homeassistant/components/igd/device.py rename to homeassistant/components/upnp/device.py index af16623e4b2..6dce3889eaf 100644 --- a/homeassistant/components/igd/device.py +++ b/homeassistant/components/upnp/device.py @@ -1,5 +1,4 @@ -"""Hass representation of an IGD.""" - +"""Hass representation of an UPnP/IGD.""" import asyncio from ipaddress import IPv4Address @@ -13,7 +12,7 @@ from .const import LOGGER as _LOGGER class Device: - """Hass representation of an IGD.""" + """Hass representation of an UPnP/IGD.""" def __init__(self, igd_device): """Initializer.""" @@ -24,7 +23,7 @@ class Device: async def async_create_device(cls, hass: HomeAssistantType, ssdp_description: str): - """Create IGD device.""" + """Create UPnP/IGD device.""" # build async_upnp_client requester from async_upnp_client.aiohttp import AiohttpSessionRequester session = async_get_clientsession(hass) diff --git a/homeassistant/components/igd/strings.json b/homeassistant/components/upnp/strings.json similarity index 77% rename from homeassistant/components/igd/strings.json rename to homeassistant/components/upnp/strings.json index 5ab50b3a7d0..829631254ad 100644 --- a/homeassistant/components/igd/strings.json +++ b/homeassistant/components/upnp/strings.json @@ -1,14 +1,14 @@ { "config": { - "title": "IGD", + "title": "UPnP/IGD", "step": { "init": { - "title": "IGD" + "title": "UPnP/IGD" }, "user": { - "title": "Configuration options for the IGD", + "title": "Configuration options for the UPnP/IGD", "data":{ - "igd": "IGD", + "igd": "UPnP/IGD", "sensors": "Add traffic sensors", "port_forward": "Enable port forward for Home Assistant" } diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 93c904d3b91..19f7c071726 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -143,7 +143,7 @@ FLOWS = [ 'nest', 'sonos', 'zone', - 'igd', + 'upnp', ] diff --git a/tests/components/igd/__init__.py b/tests/components/upnp/__init__.py similarity index 100% rename from tests/components/igd/__init__.py rename to tests/components/upnp/__init__.py diff --git a/tests/components/igd/test_config_flow.py b/tests/components/upnp/test_config_flow.py similarity index 87% rename from tests/components/igd/test_config_flow.py rename to tests/components/upnp/test_config_flow.py index e02d5630e96..f6ec05a42ab 100644 --- a/tests/components/igd/test_config_flow.py +++ b/tests/components/upnp/test_config_flow.py @@ -1,16 +1,16 @@ -"""Tests for IGD config flow.""" +"""Tests for UPnP/IGD config flow.""" -from homeassistant.components import igd -from homeassistant.components.igd import config_flow as igd_config_flow +from homeassistant.components import upnp +from homeassistant.components.upnp import config_flow as upnp_config_flow from tests.common import MockConfigEntry async def test_flow_none_discovered(hass): """Test no device discovered flow.""" - flow = igd_config_flow.IgdFlowHandler() + flow = upnp_config_flow.UpnpFlowHandler() flow.hass = hass - hass.data[igd.DOMAIN] = { + hass.data[upnp.DOMAIN] = { 'discovered': {} } @@ -21,12 +21,12 @@ async def test_flow_none_discovered(hass): async def test_flow_already_configured(hass): """Test device already configured flow.""" - flow = igd_config_flow.IgdFlowHandler() + flow = upnp_config_flow.UpnpFlowHandler() flow.hass = hass # discovered device udn = 'uuid:device_1' - hass.data[igd.DOMAIN] = { + hass.data[upnp.DOMAIN] = { 'discovered': { udn: { 'friendly_name': '192.168.1.1 (Test device)', @@ -37,7 +37,7 @@ async def test_flow_already_configured(hass): } # configured entry - MockConfigEntry(domain=igd.DOMAIN, data={ + MockConfigEntry(domain=upnp.DOMAIN, data={ 'udn': udn, 'host': '192.168.1.1', }).add_to_hass(hass) @@ -53,12 +53,12 @@ async def test_flow_already_configured(hass): async def test_flow_no_sensors_no_port_forward(hass): """Test single device, no sensors, no port_forward.""" - flow = igd_config_flow.IgdFlowHandler() + flow = upnp_config_flow.UpnpFlowHandler() flow.hass = hass # discovered device udn = 'uuid:device_1' - hass.data[igd.DOMAIN] = { + hass.data[upnp.DOMAIN] = { 'discovered': { udn: { 'friendly_name': '192.168.1.1 (Test device)', @@ -69,7 +69,7 @@ async def test_flow_no_sensors_no_port_forward(hass): } # configured entry - MockConfigEntry(domain=igd.DOMAIN, data={ + MockConfigEntry(domain=upnp.DOMAIN, data={ 'udn': udn, 'host': '192.168.1.1', }).add_to_hass(hass) @@ -85,12 +85,12 @@ async def test_flow_no_sensors_no_port_forward(hass): async def test_flow_discovered_form(hass): """Test single device discovered, show form flow.""" - flow = igd_config_flow.IgdFlowHandler() + flow = upnp_config_flow.UpnpFlowHandler() flow.hass = hass # discovered device udn = 'uuid:device_1' - hass.data[igd.DOMAIN] = { + hass.data[upnp.DOMAIN] = { 'discovered': { udn: { 'friendly_name': '192.168.1.1 (Test device)', @@ -107,13 +107,13 @@ async def test_flow_discovered_form(hass): async def test_flow_two_discovered_form(hass): """Test single device discovered, show form flow.""" - flow = igd_config_flow.IgdFlowHandler() + flow = upnp_config_flow.UpnpFlowHandler() flow.hass = hass # discovered device udn_1 = 'uuid:device_1' udn_2 = 'uuid:device_2' - hass.data[igd.DOMAIN] = { + hass.data[upnp.DOMAIN] = { 'discovered': { udn_1: { 'friendly_name': '192.168.1.1 (Test device)', @@ -145,11 +145,11 @@ async def test_flow_two_discovered_form(hass): async def test_config_entry_created(hass): """Test config entry is created.""" - flow = igd_config_flow.IgdFlowHandler() + flow = upnp_config_flow.UpnpFlowHandler() flow.hass = hass # discovered device - hass.data[igd.DOMAIN] = { + hass.data[upnp.DOMAIN] = { 'discovered': { 'uuid:device_1': { 'friendly_name': '192.168.1.1 (Test device)', @@ -178,11 +178,11 @@ async def test_config_entry_created(hass): async def test_flow_discovery_auto_config_sensors(hass): """Test creation of device with auto_config.""" - flow = igd_config_flow.IgdFlowHandler() + flow = upnp_config_flow.UpnpFlowHandler() flow.hass = hass # auto_config active - hass.data[igd.DOMAIN] = { + hass.data[upnp.DOMAIN] = { 'auto_config': { 'active': True, 'port_forward': False, @@ -210,11 +210,11 @@ async def test_flow_discovery_auto_config_sensors(hass): async def test_flow_discovery_auto_config_sensors_port_forward(hass): """Test creation of device with auto_config, with port forward.""" - flow = igd_config_flow.IgdFlowHandler() + flow = upnp_config_flow.UpnpFlowHandler() flow.hass = hass # auto_config active, with port_forward - hass.data[igd.DOMAIN] = { + hass.data[upnp.DOMAIN] = { 'auto_config': { 'active': True, 'port_forward': True, diff --git a/tests/components/igd/test_init.py b/tests/components/upnp/test_init.py similarity index 75% rename from tests/components/igd/test_init.py rename to tests/components/upnp/test_init.py index 048313affb7..581abc3190c 100644 --- a/tests/components/igd/test_init.py +++ b/tests/components/upnp/test_init.py @@ -1,11 +1,11 @@ -"""Test IGD setup process.""" +"""Test UPnP/IGD setup process.""" from ipaddress import ip_address from unittest.mock import patch, MagicMock from homeassistant.setup import async_setup_component -from homeassistant.components import igd -from homeassistant.components.igd.device import Device +from homeassistant.components import upnp +from homeassistant.components.upnp.device import Device from homeassistant.const import EVENT_HOMEASSISTANT_STOP from tests.common import MockConfigEntry @@ -49,9 +49,9 @@ class MockDevice(Device): async def test_async_setup_no_auto_config(hass): """Test async_setup.""" # setup component, enable auto_config - await async_setup_component(hass, 'igd') + await async_setup_component(hass, 'upnp') - assert hass.data[igd.DOMAIN]['auto_config'] == { + assert hass.data[upnp.DOMAIN]['auto_config'] == { 'active': False, 'port_forward': False, 'ports': {'hass': 'hass'}, @@ -62,9 +62,9 @@ async def test_async_setup_no_auto_config(hass): async def test_async_setup_auto_config(hass): """Test async_setup.""" # setup component, enable auto_config - await async_setup_component(hass, 'igd', {'igd': {}, 'discovery': {}}) + await async_setup_component(hass, 'upnp', {'upnp': {}, 'discovery': {}}) - assert hass.data[igd.DOMAIN]['auto_config'] == { + assert hass.data[upnp.DOMAIN]['auto_config'] == { 'active': True, 'port_forward': False, 'ports': {'hass': 'hass'}, @@ -75,14 +75,14 @@ async def test_async_setup_auto_config(hass): async def test_async_setup_auto_config_port_forward(hass): """Test async_setup.""" # setup component, enable auto_config - await async_setup_component(hass, 'igd', { - 'igd': { + await async_setup_component(hass, 'upnp', { + 'upnp': { 'port_forward': True, 'ports': {'hass': 'hass'}, }, 'discovery': {}}) - assert hass.data[igd.DOMAIN]['auto_config'] == { + assert hass.data[upnp.DOMAIN]['auto_config'] == { 'active': True, 'port_forward': True, 'ports': {'hass': 'hass'}, @@ -93,11 +93,11 @@ async def test_async_setup_auto_config_port_forward(hass): async def test_async_setup_auto_config_no_sensors(hass): """Test async_setup.""" # setup component, enable auto_config - await async_setup_component(hass, 'igd', { - 'igd': {'sensors': False}, + await async_setup_component(hass, 'upnp', { + 'upnp': {'sensors': False}, 'discovery': {}}) - assert hass.data[igd.DOMAIN]['auto_config'] == { + assert hass.data[upnp.DOMAIN]['auto_config'] == { 'active': True, 'port_forward': False, 'ports': {'hass': 'hass'}, @@ -108,7 +108,7 @@ async def test_async_setup_auto_config_no_sensors(hass): async def test_async_setup_entry_default(hass): """Test async_setup_entry.""" udn = 'uuid:device_1' - entry = MockConfigEntry(domain=igd.DOMAIN, data={ + entry = MockConfigEntry(domain=upnp.DOMAIN, data={ 'ssdp_description': 'http://192.168.1.1/desc.xml', 'udn': udn, 'sensors': True, @@ -116,9 +116,9 @@ async def test_async_setup_entry_default(hass): }) # ensure hass.http is available - await async_setup_component(hass, 'igd') + await async_setup_component(hass, 'upnp') - # mock homeassistant.components.igd.device.Device + # mock homeassistant.components.upnp.device.Device mock_device = MagicMock() mock_device.udn = udn mock_device.async_add_port_mappings.return_value = mock_coro() @@ -126,18 +126,18 @@ async def test_async_setup_entry_default(hass): with patch.object(Device, 'async_create_device') as mock_create_device: mock_create_device.return_value = mock_coro( return_value=mock_device) - with patch('homeassistant.components.igd.device.get_local_ip', + with patch('homeassistant.components.upnp.device.get_local_ip', return_value='192.168.1.10'): - assert await igd.async_setup_entry(hass, entry) is True + assert await upnp.async_setup_entry(hass, entry) is True # ensure device is stored/used - assert hass.data[igd.DOMAIN]['devices'][udn] == mock_device + assert hass.data[upnp.DOMAIN]['devices'][udn] == mock_device hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) await hass.async_block_till_done() # ensure cleaned up - assert udn not in hass.data[igd.DOMAIN]['devices'] + assert udn not in hass.data[upnp.DOMAIN]['devices'] # ensure no port-mapping-methods called assert len(mock_device.async_add_port_mappings.mock_calls) == 0 @@ -147,7 +147,7 @@ async def test_async_setup_entry_default(hass): async def test_async_setup_entry_port_forward(hass): """Test async_setup_entry.""" udn = 'uuid:device_1' - entry = MockConfigEntry(domain=igd.DOMAIN, data={ + entry = MockConfigEntry(domain=upnp.DOMAIN, data={ 'ssdp_description': 'http://192.168.1.1/desc.xml', 'udn': udn, 'sensors': False, @@ -155,8 +155,8 @@ async def test_async_setup_entry_port_forward(hass): }) # ensure hass.http is available - await async_setup_component(hass, 'igd', { - 'igd': { + await async_setup_component(hass, 'upnp', { + 'upnp': { 'port_forward': True, 'ports': {'hass': 'hass'}, }, @@ -166,12 +166,12 @@ async def test_async_setup_entry_port_forward(hass): mock_device = MockDevice(udn) with patch.object(Device, 'async_create_device') as mock_create_device: mock_create_device.return_value = mock_coro(return_value=mock_device) - with patch('homeassistant.components.igd.device.get_local_ip', + with patch('homeassistant.components.upnp.device.get_local_ip', return_value='192.168.1.10'): - assert await igd.async_setup_entry(hass, entry) is True + assert await upnp.async_setup_entry(hass, entry) is True # ensure device is stored/used - assert hass.data[igd.DOMAIN]['devices'][udn] == mock_device + assert hass.data[upnp.DOMAIN]['devices'][udn] == mock_device # ensure add-port-mapping-methods called assert mock_device.added_port_mappings == [ @@ -182,7 +182,7 @@ async def test_async_setup_entry_port_forward(hass): await hass.async_block_till_done() # ensure cleaned up - assert udn not in hass.data[igd.DOMAIN]['devices'] + assert udn not in hass.data[upnp.DOMAIN]['devices'] # ensure delete-port-mapping-methods called assert mock_device.removed_port_mappings == [8123] From a7a16e4317b9b61d3e01b8ec54ad499d73a3a600 Mon Sep 17 00:00:00 2001 From: Steven Looman Date: Mon, 17 Sep 2018 22:15:44 +0200 Subject: [PATCH 017/247] Fix tracebacks --- homeassistant/components/upnp/config_flow.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/homeassistant/components/upnp/config_flow.py b/homeassistant/components/upnp/config_flow.py index 5c75d0e0517..95deed73e63 100644 --- a/homeassistant/components/upnp/config_flow.py +++ b/homeassistant/components/upnp/config_flow.py @@ -89,6 +89,8 @@ class UpnpFlowHandler(data_entry_flow.FlowHandler): async def async_step_user(self, user_input=None): """Manual set up.""" + ensure_domain_data(self.hass) + # if user input given, handle it user_input = user_input or {} if 'name' in user_input: @@ -126,10 +128,14 @@ class UpnpFlowHandler(data_entry_flow.FlowHandler): async def async_step_import(self, import_info): """Import a new UPnP/IGD as a config entry.""" + ensure_domain_data(self.hass) + return await self._async_save_entry(import_info) async def _async_save_entry(self, import_info): """Store UPNP/IGD as new entry.""" + ensure_domain_data(self.hass) + # ensure we know the host name = import_info['name'] discovery_infos = [info From 5cb0c92e78f8e7c1306931e69b4707841f3507d0 Mon Sep 17 00:00:00 2001 From: Steven Looman Date: Mon, 17 Sep 2018 22:25:32 +0200 Subject: [PATCH 018/247] Fix discovery/config entry handlers --- homeassistant/components/discovery.py | 2 +- homeassistant/components/upnp/__init__.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/discovery.py b/homeassistant/components/discovery.py index 092e8b7b578..75929e76462 100644 --- a/homeassistant/components/discovery.py +++ b/homeassistant/components/discovery.py @@ -49,7 +49,7 @@ CONFIG_ENTRY_HANDLERS = { 'google_cast': 'cast', SERVICE_HUE: 'hue', 'sonos': 'sonos', - 'upnp': 'igd', + 'igd': 'upnp', } SERVICE_HANDLERS = { diff --git a/homeassistant/components/upnp/__init__.py b/homeassistant/components/upnp/__init__.py index a39ae210131..25650e6e637 100644 --- a/homeassistant/components/upnp/__init__.py +++ b/homeassistant/components/upnp/__init__.py @@ -63,7 +63,6 @@ def _substitute_hass_ports(ports, hass_port): # config async def async_setup(hass: HomeAssistantType, config: ConfigType): """Register a port mapping for Home Assistant via UPnP.""" - _LOGGER.debug('async_setup: %s', config.get(DOMAIN)) ensure_domain_data(hass) # ensure sane config From b770acd9a73e75725c375350e443385891c2cd9c Mon Sep 17 00:00:00 2001 From: Steven Looman Date: Tue, 18 Sep 2018 20:21:52 +0200 Subject: [PATCH 019/247] Update requirements_all.txt --- requirements_all.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_all.txt b/requirements_all.txt index 01ff0fa17b2..fc9c9dd8c47 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -141,7 +141,7 @@ apns2==0.3.0 # homeassistant.components.asterisk_mbox asterisk_mbox==0.5.0 -# homeassistant.components.igd +# homeassistant.components.upnp # homeassistant.components.media_player.dlna_dmr async-upnp-client==0.12.4 From b72499a949d158fd537c6766229ffc907d7f6e84 Mon Sep 17 00:00:00 2001 From: Steven Looman Date: Tue, 18 Sep 2018 20:22:33 +0200 Subject: [PATCH 020/247] Fixes after rename --- .coveragerc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.coveragerc b/.coveragerc index 078b03f21d2..c3c6f419d17 100644 --- a/.coveragerc +++ b/.coveragerc @@ -674,7 +674,7 @@ omit = homeassistant/components/sensor/haveibeenpwned.py homeassistant/components/sensor/hp_ilo.py homeassistant/components/sensor/htu21d.py - homeassistant/components/sensor/igd.py + homeassistant/components/sensor/upnp.py homeassistant/components/sensor/imap_email_content.py homeassistant/components/sensor/imap.py homeassistant/components/sensor/influxdb.py From 6e01ea5929920b6441752c2f1746d6dae20b32b8 Mon Sep 17 00:00:00 2001 From: Steven Looman Date: Thu, 20 Sep 2018 18:15:04 +0200 Subject: [PATCH 021/247] Preserve compatibility with original upnp --- homeassistant/components/upnp/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/upnp/const.py b/homeassistant/components/upnp/const.py index 0728747af3d..ad57bc7d7f4 100644 --- a/homeassistant/components/upnp/const.py +++ b/homeassistant/components/upnp/const.py @@ -2,7 +2,7 @@ import logging -CONF_ENABLE_PORT_MAPPING = 'port_forward' +CONF_ENABLE_PORT_MAPPING = 'port_mapping' CONF_ENABLE_SENSORS = 'sensors' CONF_HASS = 'hass' CONF_LOCAL_IP = 'local_ip' From dc75db637661ec99051436654ecb4d7f3d353aa5 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 24 Sep 2018 12:15:47 +0200 Subject: [PATCH 022/247] Bumped version to 0.80.0.dev0 --- homeassistant/const.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 307cbf9fe66..0bcfcd9d4ad 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 = 79 -PATCH_VERSION = '0b0' +MINOR_VERSION = 80 +PATCH_VERSION = '0.dev0' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From b52e8525acc1c5f17c9a833c3b6b075dca6f961c Mon Sep 17 00:00:00 2001 From: randellhodges Date: Mon, 24 Sep 2018 12:09:15 -0400 Subject: [PATCH 023/247] Add mode (daily/hourly) to darksky (#16719) * added daily mode to darksky and wind_bearing, ozone, and visibility * Removed dew point and pressure until the standard is updated --- homeassistant/components/weather/__init__.py | 2 + homeassistant/components/weather/darksky.py | 77 ++++++++++++++++--- .../components/weather/openweathermap.py | 7 +- 3 files changed, 71 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/weather/__init__.py b/homeassistant/components/weather/__init__.py index a43999f2276..c2a9f4f79d1 100644 --- a/homeassistant/components/weather/__init__.py +++ b/homeassistant/components/weather/__init__.py @@ -27,6 +27,8 @@ ATTR_FORECAST_PRECIPITATION = 'precipitation' ATTR_FORECAST_TEMP = 'temperature' ATTR_FORECAST_TEMP_LOW = 'templow' ATTR_FORECAST_TIME = 'datetime' +ATTR_FORECAST_WIND_SPEED = 'wind_speed' +ATTR_FORECAST_WIND_BEARING = 'wind_bearing' ATTR_WEATHER_ATTRIBUTION = 'attribution' ATTR_WEATHER_HUMIDITY = 'humidity' ATTR_WEATHER_OZONE = 'ozone' diff --git a/homeassistant/components/weather/darksky.py b/homeassistant/components/weather/darksky.py index 34a6fd3d6f6..c753c0249ca 100644 --- a/homeassistant/components/weather/darksky.py +++ b/homeassistant/components/weather/darksky.py @@ -13,10 +13,12 @@ import voluptuous as vol from homeassistant.components.weather import ( ATTR_FORECAST_TEMP, ATTR_FORECAST_TIME, ATTR_FORECAST_CONDITION, + ATTR_FORECAST_WIND_SPEED, ATTR_FORECAST_WIND_BEARING, + ATTR_FORECAST_TEMP_LOW, ATTR_FORECAST_PRECIPITATION, PLATFORM_SCHEMA, WeatherEntity) from homeassistant.const import ( CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, TEMP_CELSIUS, - TEMP_FAHRENHEIT) + CONF_MODE, TEMP_FAHRENHEIT) import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle @@ -26,6 +28,8 @@ _LOGGER = logging.getLogger(__name__) ATTRIBUTION = "Powered by Dark Sky" +FORECAST_MODE = ['hourly', 'daily'] + MAP_CONDITION = { 'clear-day': 'sunny', 'clear-night': 'clear-night', @@ -50,6 +54,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_API_KEY): cv.string, vol.Optional(CONF_LATITUDE): cv.latitude, vol.Optional(CONF_LONGITUDE): cv.longitude, + vol.Optional(CONF_MODE, default='hourly'): vol.In(FORECAST_MODE), vol.Optional(CONF_UNITS): vol.In(['auto', 'si', 'us', 'ca', 'uk', 'uk2']), vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, }) @@ -62,6 +67,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): latitude = config.get(CONF_LATITUDE, hass.config.latitude) longitude = config.get(CONF_LONGITUDE, hass.config.longitude) name = config.get(CONF_NAME) + mode = config.get(CONF_MODE) units = config.get(CONF_UNITS) if not units: @@ -70,16 +76,17 @@ def setup_platform(hass, config, add_entities, discovery_info=None): dark_sky = DarkSkyData( config.get(CONF_API_KEY), latitude, longitude, units) - add_entities([DarkSkyWeather(name, dark_sky)], True) + add_entities([DarkSkyWeather(name, dark_sky, mode)], True) class DarkSkyWeather(WeatherEntity): """Representation of a weather condition.""" - def __init__(self, name, dark_sky): + def __init__(self, name, dark_sky, mode): """Initialize Dark Sky weather.""" self._name = name self._dark_sky = dark_sky + self._mode = mode self._ds_data = None self._ds_currently = None @@ -117,11 +124,26 @@ class DarkSkyWeather(WeatherEntity): """Return the wind speed.""" return self._ds_currently.get('windSpeed') + @property + def wind_bearing(self): + """Return the wind bearing.""" + return self._ds_currently.get('windBearing') + + @property + def ozone(self): + """Return the ozone level.""" + return self._ds_currently.get('ozone') + @property def pressure(self): """Return the pressure.""" return self._ds_currently.get('pressure') + @property + def visibility(self): + """Return the visibility.""" + return self._ds_currently.get('visibility') + @property def condition(self): """Return the weather condition.""" @@ -130,14 +152,47 @@ class DarkSkyWeather(WeatherEntity): @property def forecast(self): """Return the forecast array.""" - return [{ - ATTR_FORECAST_TIME: - datetime.fromtimestamp(entry.d.get('time')).isoformat(), - ATTR_FORECAST_TEMP: - entry.d.get('temperature'), - ATTR_FORECAST_CONDITION: - MAP_CONDITION.get(entry.d.get('icon')) - } for entry in self._ds_hourly.data] + # Per conversation with Joshua Reyes of Dark Sky, to get the total + # forecasted precipitation, you have to multiple the intensity by + # the hours for the forecast interval + def calc_precipitation(intensity, hours): + amount = None + if intensity is not None: + amount = round((intensity * hours), 1) + return amount if amount > 0 else None + + data = None + + if self._mode == 'daily': + data = [{ + ATTR_FORECAST_TIME: + datetime.fromtimestamp(entry.d.get('time')).isoformat(), + ATTR_FORECAST_TEMP: + entry.d.get('temperatureHigh'), + ATTR_FORECAST_TEMP_LOW: + entry.d.get('temperatureLow'), + ATTR_FORECAST_PRECIPITATION: + calc_precipitation(entry.d.get('precipIntensity'), 24), + ATTR_FORECAST_WIND_SPEED: + entry.d.get('windSpeed'), + ATTR_FORECAST_WIND_BEARING: + entry.d.get('windBearing'), + ATTR_FORECAST_CONDITION: + MAP_CONDITION.get(entry.d.get('icon')) + } for entry in self._ds_daily.data] + else: + data = [{ + ATTR_FORECAST_TIME: + datetime.fromtimestamp(entry.d.get('time')).isoformat(), + ATTR_FORECAST_TEMP: + entry.d.get('temperature'), + ATTR_FORECAST_PRECIPITATION: + calc_precipitation(entry.d.get('precipIntensity'), 1), + ATTR_FORECAST_CONDITION: + MAP_CONDITION.get(entry.d.get('icon')) + } for entry in self._ds_hourly.data] + + return data def update(self): """Get the latest data from Dark Sky.""" diff --git a/homeassistant/components/weather/openweathermap.py b/homeassistant/components/weather/openweathermap.py index b300fcbcbec..87d2bc6683f 100644 --- a/homeassistant/components/weather/openweathermap.py +++ b/homeassistant/components/weather/openweathermap.py @@ -11,7 +11,9 @@ import voluptuous as vol from homeassistant.components.weather import ( ATTR_FORECAST_CONDITION, ATTR_FORECAST_PRECIPITATION, ATTR_FORECAST_TEMP, - ATTR_FORECAST_TEMP_LOW, ATTR_FORECAST_TIME, PLATFORM_SCHEMA, WeatherEntity) + ATTR_FORECAST_TEMP_LOW, ATTR_FORECAST_TIME, ATTR_FORECAST_WIND_SPEED, + ATTR_FORECAST_WIND_BEARING, + PLATFORM_SCHEMA, WeatherEntity) from homeassistant.const import ( CONF_API_KEY, TEMP_CELSIUS, CONF_LATITUDE, CONF_LONGITUDE, CONF_MODE, CONF_NAME, STATE_UNKNOWN) @@ -22,9 +24,6 @@ REQUIREMENTS = ['pyowm==2.9.0'] _LOGGER = logging.getLogger(__name__) -ATTR_FORECAST_WIND_SPEED = 'wind_speed' -ATTR_FORECAST_WIND_BEARING = 'wind_bearing' - ATTRIBUTION = 'Data provided by OpenWeatherMap' FORECAST_MODE = ['hourly', 'daily'] From 579b77ba4cb57e1193aacc65e6fc0a5c45bc3ca1 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 25 Sep 2018 07:52:10 +0200 Subject: [PATCH 024/247] Don't warn but info when on dev mode (#16831) --- homeassistant/components/updater.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/updater.py b/homeassistant/components/updater.py index 0cb22bd98dc..4e64e3be2e6 100644 --- a/homeassistant/components/updater.py +++ b/homeassistant/components/updater.py @@ -76,7 +76,7 @@ async def async_setup(hass, config): """Set up the updater component.""" if 'dev' in current_version: # This component only makes sense in release versions - _LOGGER.warning("Running on 'dev', only analytics will be submitted") + _LOGGER.info("Running on 'dev', only analytics will be submitted") config = config.get(DOMAIN, {}) if config.get(CONF_REPORTING): From 354c8f34092073ddb7dd8719f25fde16b5a94b28 Mon Sep 17 00:00:00 2001 From: Rohan Kapoor Date: Mon, 24 Sep 2018 22:53:35 -0700 Subject: [PATCH 025/247] Bump zm-py to 0.0.3 (#16835) --- homeassistant/components/zoneminder.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zoneminder.py b/homeassistant/components/zoneminder.py index 5cfd324448b..53d6d8b2536 100644 --- a/homeassistant/components/zoneminder.py +++ b/homeassistant/components/zoneminder.py @@ -15,7 +15,7 @@ import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['zm-py==0.0.2'] +REQUIREMENTS = ['zm-py==0.0.3'] CONF_PATH_ZMS = 'path_zms' diff --git a/requirements_all.txt b/requirements_all.txt index b9550d032ab..e009f2e21a0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1567,4 +1567,4 @@ zigpy-xbee==0.1.1 zigpy==0.2.0 # homeassistant.components.zoneminder -zm-py==0.0.2 +zm-py==0.0.3 From e78f4d1b65fb4b3a448d4808543f7153091b0487 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 25 Sep 2018 08:39:35 +0200 Subject: [PATCH 026/247] Extract lovelace to it's own component (#16816) * Extract lovelace to it's own component * Lint * Update comment * Lint * Lint --- homeassistant/components/frontend/__init__.py | 36 +----- homeassistant/components/lovelace/__init__.py | 51 ++++++++ tests/components/frontend/test_init.py | 58 --------- tests/components/lovelace/__init__.py | 1 + tests/components/lovelace/test_init.py | 120 ++++++++++++++++++ 5 files changed, 173 insertions(+), 93 deletions(-) create mode 100644 homeassistant/components/lovelace/__init__.py create mode 100644 tests/components/lovelace/__init__.py create mode 100644 tests/components/lovelace/test_init.py diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index d3d25255508..362c54fabbd 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -21,16 +21,14 @@ from homeassistant.components import websocket_api from homeassistant.config import find_config_file, load_yaml_config_file from homeassistant.const import CONF_NAME, EVENT_THEMES_UPDATED from homeassistant.core import callback -from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass -from homeassistant.util.yaml import load_yaml REQUIREMENTS = ['home-assistant-frontend==20180924.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log', - 'auth', 'onboarding'] + 'auth', 'onboarding', 'lovelace'] CONF_THEMES = 'themes' CONF_EXTRA_HTML_URL = 'extra_html_url' @@ -108,10 +106,6 @@ SCHEMA_GET_TRANSLATIONS = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ vol.Required('type'): WS_TYPE_GET_TRANSLATIONS, vol.Required('language'): str, }) -WS_TYPE_GET_LOVELACE_UI = 'frontend/lovelace_config' -SCHEMA_GET_LOVELACE_UI = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ - vol.Required('type'): WS_TYPE_GET_LOVELACE_UI, -}) class Panel: @@ -208,9 +202,6 @@ async def async_setup(hass, config): hass.components.websocket_api.async_register_command( WS_TYPE_GET_TRANSLATIONS, websocket_get_translations, SCHEMA_GET_TRANSLATIONS) - hass.components.websocket_api.async_register_command( - WS_TYPE_GET_LOVELACE_UI, websocket_lovelace_config, - SCHEMA_GET_LOVELACE_UI) hass.http.register_view(ManifestJSONView) conf = config.get(DOMAIN, {}) @@ -530,28 +521,3 @@ def websocket_get_translations(hass, connection, msg): )) hass.async_add_job(send_translations()) - - -def websocket_lovelace_config(hass, connection, msg): - """Send lovelace UI config over websocket config.""" - async def send_exp_config(): - """Send lovelace frontend config.""" - error = None - try: - config = await hass.async_add_job( - load_yaml, hass.config.path('ui-lovelace.yaml')) - message = websocket_api.result_message( - msg['id'], config - ) - except FileNotFoundError: - error = ('file_not_found', - 'Could not find ui-lovelace.yaml in your config dir.') - except HomeAssistantError as err: - error = 'load_error', str(err) - - if error is not None: - message = websocket_api.error_message(msg['id'], *error) - - connection.send_message_outside(message) - - hass.async_add_job(send_exp_config()) diff --git a/homeassistant/components/lovelace/__init__.py b/homeassistant/components/lovelace/__init__.py new file mode 100644 index 00000000000..4336f50def4 --- /dev/null +++ b/homeassistant/components/lovelace/__init__.py @@ -0,0 +1,51 @@ +"""Lovelace UI.""" +import voluptuous as vol + +from homeassistant.components import websocket_api +from homeassistant.util.yaml import load_yaml +from homeassistant.exceptions import HomeAssistantError + +DOMAIN = 'lovelace' + +OLD_WS_TYPE_GET_LOVELACE_UI = 'frontend/lovelace_config' +WS_TYPE_GET_LOVELACE_UI = 'lovelace/config' +SCHEMA_GET_LOVELACE_UI = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ + vol.Required('type'): vol.Any(WS_TYPE_GET_LOVELACE_UI, + OLD_WS_TYPE_GET_LOVELACE_UI), +}) + + +async def async_setup(hass, config): + """Set up the Lovelace commands.""" + # Backwards compat. Added in 0.80. Remove after 0.85 + hass.components.websocket_api.async_register_command( + OLD_WS_TYPE_GET_LOVELACE_UI, websocket_lovelace_config, + SCHEMA_GET_LOVELACE_UI) + + hass.components.websocket_api.async_register_command( + WS_TYPE_GET_LOVELACE_UI, websocket_lovelace_config, + SCHEMA_GET_LOVELACE_UI) + + return True + + +@websocket_api.async_response +async def websocket_lovelace_config(hass, connection, msg): + """Send lovelace UI config over websocket config.""" + error = None + try: + config = await hass.async_add_job( + load_yaml, hass.config.path('ui-lovelace.yaml')) + message = websocket_api.result_message( + msg['id'], config + ) + except FileNotFoundError: + error = ('file_not_found', + 'Could not find ui-lovelace.yaml in your config dir.') + except HomeAssistantError as err: + error = 'load_error', str(err) + + if error is not None: + message = websocket_api.error_message(msg['id'], *error) + + connection.send_message_outside(message) diff --git a/tests/components/frontend/test_init.py b/tests/components/frontend/test_init.py index 17bf3d953ef..e4daf686bfe 100644 --- a/tests/components/frontend/test_init.py +++ b/tests/components/frontend/test_init.py @@ -5,7 +5,6 @@ from unittest.mock import patch import pytest -from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import async_setup_component from homeassistant.components.frontend import ( DOMAIN, CONF_JS_VERSION, CONF_THEMES, CONF_EXTRA_HTML_URL, @@ -281,63 +280,6 @@ async def test_get_translations(hass, hass_ws_client): assert msg['result'] == {'resources': {'lang': 'nl'}} -async def test_lovelace_ui(hass, hass_ws_client): - """Test lovelace_ui command.""" - await async_setup_component(hass, 'frontend') - client = await hass_ws_client(hass) - - with patch('homeassistant.components.frontend.load_yaml', - return_value={'hello': 'world'}): - await client.send_json({ - 'id': 5, - 'type': 'frontend/lovelace_config', - }) - msg = await client.receive_json() - - assert msg['id'] == 5 - assert msg['type'] == wapi.TYPE_RESULT - assert msg['success'] - assert msg['result'] == {'hello': 'world'} - - -async def test_lovelace_ui_not_found(hass, hass_ws_client): - """Test lovelace_ui command cannot find file.""" - await async_setup_component(hass, 'frontend') - client = await hass_ws_client(hass) - - with patch('homeassistant.components.frontend.load_yaml', - side_effect=FileNotFoundError): - await client.send_json({ - 'id': 5, - 'type': 'frontend/lovelace_config', - }) - msg = await client.receive_json() - - assert msg['id'] == 5 - assert msg['type'] == wapi.TYPE_RESULT - assert msg['success'] is False - assert msg['error']['code'] == 'file_not_found' - - -async def test_lovelace_ui_load_err(hass, hass_ws_client): - """Test lovelace_ui command cannot find file.""" - await async_setup_component(hass, 'frontend') - client = await hass_ws_client(hass) - - with patch('homeassistant.components.frontend.load_yaml', - side_effect=HomeAssistantError): - await client.send_json({ - 'id': 5, - 'type': 'frontend/lovelace_config', - }) - msg = await client.receive_json() - - assert msg['id'] == 5 - assert msg['type'] == wapi.TYPE_RESULT - assert msg['success'] is False - assert msg['error']['code'] == 'load_error' - - async def test_auth_load(mock_http_client): """Test auth component loaded by default.""" resp = await mock_http_client.get('/auth/providers') diff --git a/tests/components/lovelace/__init__.py b/tests/components/lovelace/__init__.py new file mode 100644 index 00000000000..fea220146ca --- /dev/null +++ b/tests/components/lovelace/__init__.py @@ -0,0 +1 @@ +"""Tests for Lovelace.""" diff --git a/tests/components/lovelace/test_init.py b/tests/components/lovelace/test_init.py new file mode 100644 index 00000000000..3bb7c0675ea --- /dev/null +++ b/tests/components/lovelace/test_init.py @@ -0,0 +1,120 @@ +"""Test the Lovelace initialization.""" +from unittest.mock import patch + +from homeassistant.exceptions import HomeAssistantError +from homeassistant.setup import async_setup_component +from homeassistant.components import websocket_api as wapi + + +async def test_deprecated_lovelace_ui(hass, hass_ws_client): + """Test lovelace_ui command.""" + await async_setup_component(hass, 'lovelace') + client = await hass_ws_client(hass) + + with patch('homeassistant.components.lovelace.load_yaml', + return_value={'hello': 'world'}): + await client.send_json({ + 'id': 5, + 'type': 'frontend/lovelace_config', + }) + msg = await client.receive_json() + + assert msg['id'] == 5 + assert msg['type'] == wapi.TYPE_RESULT + assert msg['success'] + assert msg['result'] == {'hello': 'world'} + + +async def test_deprecated_lovelace_ui_not_found(hass, hass_ws_client): + """Test lovelace_ui command cannot find file.""" + await async_setup_component(hass, 'lovelace') + client = await hass_ws_client(hass) + + with patch('homeassistant.components.lovelace.load_yaml', + side_effect=FileNotFoundError): + await client.send_json({ + 'id': 5, + 'type': 'frontend/lovelace_config', + }) + msg = await client.receive_json() + + assert msg['id'] == 5 + assert msg['type'] == wapi.TYPE_RESULT + assert msg['success'] is False + assert msg['error']['code'] == 'file_not_found' + + +async def test_deprecated_lovelace_ui_load_err(hass, hass_ws_client): + """Test lovelace_ui command cannot find file.""" + await async_setup_component(hass, 'lovelace') + client = await hass_ws_client(hass) + + with patch('homeassistant.components.lovelace.load_yaml', + side_effect=HomeAssistantError): + await client.send_json({ + 'id': 5, + 'type': 'frontend/lovelace_config', + }) + msg = await client.receive_json() + + assert msg['id'] == 5 + assert msg['type'] == wapi.TYPE_RESULT + assert msg['success'] is False + assert msg['error']['code'] == 'load_error' + + +async def test_lovelace_ui(hass, hass_ws_client): + """Test lovelace_ui command.""" + await async_setup_component(hass, 'lovelace') + client = await hass_ws_client(hass) + + with patch('homeassistant.components.lovelace.load_yaml', + return_value={'hello': 'world'}): + await client.send_json({ + 'id': 5, + 'type': 'lovelace/config', + }) + msg = await client.receive_json() + + assert msg['id'] == 5 + assert msg['type'] == wapi.TYPE_RESULT + assert msg['success'] + assert msg['result'] == {'hello': 'world'} + + +async def test_lovelace_ui_not_found(hass, hass_ws_client): + """Test lovelace_ui command cannot find file.""" + await async_setup_component(hass, 'lovelace') + client = await hass_ws_client(hass) + + with patch('homeassistant.components.lovelace.load_yaml', + side_effect=FileNotFoundError): + await client.send_json({ + 'id': 5, + 'type': 'lovelace/config', + }) + msg = await client.receive_json() + + assert msg['id'] == 5 + assert msg['type'] == wapi.TYPE_RESULT + assert msg['success'] is False + assert msg['error']['code'] == 'file_not_found' + + +async def test_lovelace_ui_load_err(hass, hass_ws_client): + """Test lovelace_ui command cannot find file.""" + await async_setup_component(hass, 'lovelace') + client = await hass_ws_client(hass) + + with patch('homeassistant.components.lovelace.load_yaml', + side_effect=HomeAssistantError): + await client.send_json({ + 'id': 5, + 'type': 'lovelace/config', + }) + msg = await client.receive_json() + + assert msg['id'] == 5 + assert msg['type'] == wapi.TYPE_RESULT + assert msg['success'] is False + assert msg['error']['code'] == 'load_error' From 42790d3e970ebe82c4a5a4358ed0ae7136f88a48 Mon Sep 17 00:00:00 2001 From: emontnemery Date: Tue, 25 Sep 2018 08:44:14 +0200 Subject: [PATCH 027/247] Remove discovered MQTT alarm_control_panel device when discovery topic is cleared (#16825) --- .../components/alarm_control_panel/mqtt.py | 28 +++++++++++++------ tests/components/mqtt/test_discovery.py | 25 +++++++++++++++++ 2 files changed, 44 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/alarm_control_panel/mqtt.py b/homeassistant/components/alarm_control_panel/mqtt.py index e5ad54c4147..e36765b2460 100644 --- a/homeassistant/components/alarm_control_panel/mqtt.py +++ b/homeassistant/components/alarm_control_panel/mqtt.py @@ -18,9 +18,9 @@ from homeassistant.const import ( STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED, STATE_UNKNOWN, CONF_NAME, CONF_CODE) from homeassistant.components.mqtt import ( - CONF_AVAILABILITY_TOPIC, CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, - CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, - CONF_RETAIN, MqttAvailability) + 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) import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -53,6 +53,10 @@ def async_setup_platform(hass, config, async_add_entities, if discovery_info is not None: config = PLATFORM_SCHEMA(discovery_info) + discovery_hash = None + if discovery_info is not None and ATTR_DISCOVERY_HASH in discovery_info: + discovery_hash = discovery_info[ATTR_DISCOVERY_HASH] + async_add_entities([MqttAlarm( config.get(CONF_NAME), config.get(CONF_STATE_TOPIC), @@ -65,18 +69,22 @@ def async_setup_platform(hass, config, async_add_entities, config.get(CONF_CODE), config.get(CONF_AVAILABILITY_TOPIC), config.get(CONF_PAYLOAD_AVAILABLE), - config.get(CONF_PAYLOAD_NOT_AVAILABLE))]) + config.get(CONF_PAYLOAD_NOT_AVAILABLE), + discovery_hash,)]) -class MqttAlarm(MqttAvailability, alarm.AlarmControlPanel): +class MqttAlarm(MqttAvailability, MqttDiscoveryUpdate, + alarm.AlarmControlPanel): """Representation of a MQTT alarm status.""" def __init__(self, name, state_topic, command_topic, qos, retain, payload_disarm, payload_arm_home, payload_arm_away, code, - availability_topic, payload_available, payload_not_available): + availability_topic, payload_available, payload_not_available, + discovery_hash): """Init the MQTT Alarm Control Panel.""" - super().__init__(availability_topic, qos, payload_available, - payload_not_available) + MqttAvailability.__init__(self, availability_topic, qos, + payload_available, payload_not_available) + MqttDiscoveryUpdate.__init__(self, discovery_hash) self._state = STATE_UNKNOWN self._name = name self._state_topic = state_topic @@ -87,11 +95,13 @@ class MqttAlarm(MqttAvailability, alarm.AlarmControlPanel): self._payload_arm_home = payload_arm_home self._payload_arm_away = payload_arm_away self._code = code + self._discovery_hash = discovery_hash @asyncio.coroutine def async_added_to_hass(self): """Subscribe mqtt events.""" - yield from super().async_added_to_hass() + yield from MqttAvailability.async_added_to_hass(self) + yield from MqttDiscoveryUpdate.async_added_to_hass(self) @callback def message_received(topic, payload, qos): diff --git a/tests/components/mqtt/test_discovery.py b/tests/components/mqtt/test_discovery.py index 6de277eb48d..ee3460d46b5 100644 --- a/tests/components/mqtt/test_discovery.py +++ b/tests/components/mqtt/test_discovery.py @@ -209,3 +209,28 @@ def test_discovery_removal(hass, mqtt_mock, caplog): state = hass.states.get('switch.beer') assert state is None + + +@asyncio.coroutine +def test_discovery_removal_alarm(hass, mqtt_mock, caplog): + """Test removal of discovered alarm_control_panel.""" + yield from async_start(hass, 'homeassistant', {}) + data = ( + '{ "name": "Beer",' + ' "status_topic": "test_topic",' + ' "command_topic": "test_topic" }' + ) + async_fire_mqtt_message(hass, + 'homeassistant/alarm_control_panel/bla/config', + data) + yield from hass.async_block_till_done() + state = hass.states.get('alarm_control_panel.beer') + assert state is not None + assert state.name == 'Beer' + async_fire_mqtt_message(hass, + 'homeassistant/alarm_control_panel/bla/config', + '') + yield from hass.async_block_till_done() + yield from hass.async_block_till_done() + state = hass.states.get('alarm_control_panel.beer') + assert state is None From 90197b6ec983bd5be5311d0410379bc5d17b1631 Mon Sep 17 00:00:00 2001 From: emontnemery Date: Tue, 25 Sep 2018 09:19:04 +0200 Subject: [PATCH 028/247] Remove discovered MQTT light device when discovery topic is cleared (#16824) --- homeassistant/components/light/mqtt.py | 24 ++++++++++------ tests/components/mqtt/test_discovery.py | 37 +++++++++++++++++++++++-- 2 files changed, 51 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/light/mqtt.py b/homeassistant/components/light/mqtt.py index 64331411f7f..10227bdc3b6 100644 --- a/homeassistant/components/light/mqtt.py +++ b/homeassistant/components/light/mqtt.py @@ -19,9 +19,9 @@ from homeassistant.const import ( CONF_OPTIMISTIC, CONF_PAYLOAD_OFF, CONF_PAYLOAD_ON, STATE_ON, CONF_RGB, CONF_STATE, CONF_VALUE_TEMPLATE, CONF_WHITE_VALUE, CONF_XY) from homeassistant.components.mqtt import ( - CONF_AVAILABILITY_TOPIC, CONF_COMMAND_TOPIC, CONF_PAYLOAD_AVAILABLE, - CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, - MqttAvailability) + 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) from homeassistant.helpers.restore_state import async_get_last_state import homeassistant.helpers.config_validation as cv import homeassistant.util.color as color_util @@ -111,6 +111,10 @@ async def async_setup_platform(hass, config, async_add_entities, config.setdefault( CONF_STATE_VALUE_TEMPLATE, config.get(CONF_VALUE_TEMPLATE)) + discovery_hash = None + if discovery_info is not None and ATTR_DISCOVERY_HASH in discovery_info: + discovery_hash = discovery_info[ATTR_DISCOVERY_HASH] + async_add_entities([MqttLight( config.get(CONF_NAME), config.get(CONF_UNIQUE_ID), @@ -156,19 +160,21 @@ async def async_setup_platform(hass, config, async_add_entities, config.get(CONF_AVAILABILITY_TOPIC), config.get(CONF_PAYLOAD_AVAILABLE), config.get(CONF_PAYLOAD_NOT_AVAILABLE), + discovery_hash, )]) -class MqttLight(MqttAvailability, Light): +class MqttLight(MqttAvailability, MqttDiscoveryUpdate, Light): """Representation of a MQTT light.""" def __init__(self, name, unique_id, effect_list, topic, templates, qos, retain, payload, optimistic, brightness_scale, white_value_scale, on_command_type, availability_topic, - payload_available, payload_not_available): + payload_available, payload_not_available, discovery_hash): """Initialize MQTT light.""" - super().__init__(availability_topic, qos, payload_available, - payload_not_available) + MqttAvailability.__init__(self, availability_topic, qos, + payload_available, payload_not_available) + MqttDiscoveryUpdate.__init__(self, discovery_hash) self._name = name self._unique_id = unique_id self._effect_list = effect_list @@ -216,10 +222,12 @@ class MqttLight(MqttAvailability, Light): SUPPORT_WHITE_VALUE) self._supported_features |= ( topic[CONF_XY_COMMAND_TOPIC] is not None and SUPPORT_COLOR) + self._discovery_hash = discovery_hash async def async_added_to_hass(self): """Subscribe to MQTT events.""" - await super().async_added_to_hass() + await MqttAvailability.async_added_to_hass(self) + await MqttDiscoveryUpdate.async_added_to_hass(self) templates = {} for key, tpl in list(self._templates.items()): diff --git a/tests/components/mqtt/test_discovery.py b/tests/components/mqtt/test_discovery.py index ee3460d46b5..9e334719a36 100644 --- a/tests/components/mqtt/test_discovery.py +++ b/tests/components/mqtt/test_discovery.py @@ -212,25 +212,58 @@ def test_discovery_removal(hass, mqtt_mock, caplog): @asyncio.coroutine -def test_discovery_removal_alarm(hass, mqtt_mock, caplog): - """Test removal of discovered alarm_control_panel.""" +def test_discovery_removal_light(hass, mqtt_mock, caplog): + """Test removal of discovered light.""" yield from async_start(hass, 'homeassistant', {}) + data = ( '{ "name": "Beer",' ' "status_topic": "test_topic",' ' "command_topic": "test_topic" }' ) + + async_fire_mqtt_message(hass, 'homeassistant/light/bla/config', + data) + yield from hass.async_block_till_done() + + state = hass.states.get('light.beer') + assert state is not None + assert state.name == 'Beer' + + async_fire_mqtt_message(hass, 'homeassistant/light/bla/config', + '') + yield from hass.async_block_till_done() + yield from hass.async_block_till_done() + + state = hass.states.get('light.beer') + assert state is None + + +@asyncio.coroutine +def test_discovery_removal_alarm(hass, mqtt_mock, caplog): + """Test removal of discovered alarm_control_panel.""" + yield from async_start(hass, 'homeassistant', {}) + + data = ( + '{ "name": "Beer",' + ' "status_topic": "test_topic",' + ' "command_topic": "test_topic" }' + ) + async_fire_mqtt_message(hass, 'homeassistant/alarm_control_panel/bla/config', data) yield from hass.async_block_till_done() + state = hass.states.get('alarm_control_panel.beer') assert state is not None assert state.name == 'Beer' + async_fire_mqtt_message(hass, 'homeassistant/alarm_control_panel/bla/config', '') yield from hass.async_block_till_done() yield from hass.async_block_till_done() + state = hass.states.get('alarm_control_panel.beer') assert state is None From 069b819679c21226f9bf873eb5be9fdc8e3a97f4 Mon Sep 17 00:00:00 2001 From: sander76 Date: Tue, 25 Sep 2018 10:15:03 +0200 Subject: [PATCH 029/247] Add unique_id to homematic_cloud (#16828) * add unique_id * add docstring and trigger travis --- homeassistant/components/homematicip_cloud/device.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/homematicip_cloud/device.py b/homeassistant/components/homematicip_cloud/device.py index 9c335befda4..6a4f958387e 100644 --- a/homeassistant/components/homematicip_cloud/device.py +++ b/homeassistant/components/homematicip_cloud/device.py @@ -61,6 +61,11 @@ class HomematicipGenericDevice(Entity): """Device available.""" return not self._device.unreach + @property + def unique_id(self): + """Return a unique ID.""" + return "{}_{}".format(self.__class__.__name__, self._device.id) + @property def icon(self): """Return the icon.""" From 4f9fc9b39fb08c5bc02d3025be875b02db75e60f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 25 Sep 2018 10:16:30 +0200 Subject: [PATCH 030/247] Don't create entity registry in tests (#16838) --- tests/components/zwave/test_init.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/components/zwave/test_init.py b/tests/components/zwave/test_init.py index 1857d14ad84..bc96c90d50a 100644 --- a/tests/components/zwave/test_init.py +++ b/tests/components/zwave/test_init.py @@ -749,9 +749,11 @@ class TestZWaveDeviceEntityValues(unittest.TestCase): self.device_config = {'mock_component.registry_id': { zwave.CONF_IGNORED: True }} - self.registry.async_get_or_create( - 'mock_component', zwave.DOMAIN, '567-1000', - suggested_object_id='registry_id') + with patch.object(self.registry, 'async_schedule_save'): + self.registry.async_get_or_create( + 'mock_component', zwave.DOMAIN, '567-1000', + suggested_object_id='registry_id') + zwave.ZWaveDeviceEntityValues( hass=self.hass, schema=self.mock_schema, From 03bce84c32e4d1962e07c39f0afab2c78971efda Mon Sep 17 00:00:00 2001 From: GP8x Date: Tue, 25 Sep 2018 10:14:42 +0100 Subject: [PATCH 031/247] Add additional Netatmo public data sensors (#16671) Update deps --- homeassistant/components/netatmo.py | 2 +- .../components/sensor/netatmo_public.py | 112 ++++++++++++------ requirements_all.txt | 2 +- 3 files changed, 80 insertions(+), 36 deletions(-) diff --git a/homeassistant/components/netatmo.py b/homeassistant/components/netatmo.py index 59b0a64f6e9..d8924c6c301 100644 --- a/homeassistant/components/netatmo.py +++ b/homeassistant/components/netatmo.py @@ -16,7 +16,7 @@ from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle -REQUIREMENTS = ['pyatmo==1.1.1'] +REQUIREMENTS = ['pyatmo==1.2'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sensor/netatmo_public.py b/homeassistant/components/sensor/netatmo_public.py index d1c6e03d1b0..59b0b317ed1 100644 --- a/homeassistant/components/sensor/netatmo_public.py +++ b/homeassistant/components/sensor/netatmo_public.py @@ -10,7 +10,9 @@ import logging import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import (CONF_NAME, CONF_TYPE) +from homeassistant.const import ( + CONF_NAME, CONF_MODE, CONF_MONITORED_CONDITIONS, TEMP_CELSIUS, + DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_HUMIDITY, STATE_UNKNOWN) from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle @@ -26,13 +28,22 @@ CONF_LAT_SW = 'lat_sw' CONF_LON_SW = 'lon_sw' DEFAULT_NAME = 'Netatmo Public Data' -DEFAULT_TYPE = 'max' -SENSOR_TYPES = {'max', 'avg'} +DEFAULT_MODE = 'avg' +MODE_TYPES = {'max', 'avg'} + +SENSOR_TYPES = { + 'temperature': ['Temperature', TEMP_CELSIUS, 'mdi:thermometer', + DEVICE_CLASS_TEMPERATURE], + 'pressure': ['Pressure', 'mbar', 'mdi:gauge', None], + 'humidity': ['Humidity', '%', 'mdi:water-percent', DEVICE_CLASS_HUMIDITY], + 'rain': ['Rain', 'mm', 'mdi:weather-rainy', None], + 'windstrength': ['Wind Strength', 'km/h', 'mdi:weather-windy', None], + 'guststrength': ['Gust Strength', 'km/h', 'mdi:weather-windy', None], +} # NetAtmo Data is uploaded to server every 10 minutes MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=600) - PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_AREAS): vol.All(cv.ensure_list, [ { @@ -40,9 +51,9 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_LAT_SW): cv.latitude, vol.Required(CONF_LON_NE): cv.longitude, vol.Required(CONF_LON_SW): cv.longitude, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_TYPE, default=DEFAULT_TYPE): - vol.In(SENSOR_TYPES) + vol.Required(CONF_MONITORED_CONDITIONS): [vol.In(SENSOR_TYPES)], + vol.Optional(CONF_MODE, default=DEFAULT_MODE): vol.In(MODE_TYPES), + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string } ]), }) @@ -59,20 +70,29 @@ def setup_platform(hass, config, add_entities, discovery_info=None): lat_ne=area_conf.get(CONF_LAT_NE), lon_ne=area_conf.get(CONF_LON_NE), lat_sw=area_conf.get(CONF_LAT_SW), - lon_sw=area_conf.get(CONF_LON_SW), - calculation=area_conf.get(CONF_TYPE)) - sensors.append(NetatmoPublicSensor(area_conf.get(CONF_NAME), data)) - add_entities(sensors) + lon_sw=area_conf.get(CONF_LON_SW)) + for sensor_type in area_conf.get(CONF_MONITORED_CONDITIONS): + sensors.append(NetatmoPublicSensor(area_conf.get(CONF_NAME), + data, sensor_type, + area_conf.get(CONF_MODE))) + add_entities(sensors, True) class NetatmoPublicSensor(Entity): """Represent a single sensor in a Netatmo.""" - def __init__(self, name, data): + def __init__(self, area_name, data, sensor_type, mode): """Initialize the sensor.""" self.netatmo_data = data - self._name = name + self.type = sensor_type + self._mode = mode + self._name = '{} {}'.format(area_name, + SENSOR_TYPES[self.type][0]) + self._area_name = area_name self._state = None + self._device_class = SENSOR_TYPES[self.type][3] + self._icon = SENSOR_TYPES[self.type][2] + self._unit_of_measurement = SENSOR_TYPES[self.type][1] @property def name(self): @@ -82,33 +102,63 @@ class NetatmoPublicSensor(Entity): @property def icon(self): """Icon to use in the frontend.""" - return 'mdi:weather-rainy' + return self._icon @property def device_class(self): """Return the device class of the sensor.""" - return None + return self._device_class @property def state(self): - """Return true if binary sensor is on.""" + """Return the state of the device.""" return self._state @property def unit_of_measurement(self): """Return the unit of measurement of this entity.""" - return 'mm' + return self._unit_of_measurement def update(self): """Get the latest data from NetAtmo API and updates the states.""" self.netatmo_data.update() - self._state = self.netatmo_data.data + + if self.netatmo_data.data is None: + _LOGGER.warning("No data found for %s", self._name) + self._state = STATE_UNKNOWN + return + + data = None + + if self.type == 'temperature': + data = self.netatmo_data.data.getLatestTemperatures() + elif self.type == 'pressure': + data = self.netatmo_data.data.getLatestPressures() + elif self.type == 'humidity': + data = self.netatmo_data.data.getLatestHumidities() + elif self.type == 'rain': + data = self.netatmo_data.data.getLatestRain() + elif self.type == 'windstrength': + data = self.netatmo_data.data.getLatestWindStrengths() + elif self.type == 'guststrength': + data = self.netatmo_data.data.getLatestGustStrengths() + + if not data: + _LOGGER.warning("No station provides %s data in the area %s", + self.type, self._area_name) + self._state = STATE_UNKNOWN + return + + if self._mode == 'avg': + self._state = round(sum(data.values()) / len(data), 1) + elif self._mode == 'max': + self._state = max(data.values()) class NetatmoPublicData: """Get the latest data from NetAtmo.""" - def __init__(self, auth, lat_ne, lon_ne, lat_sw, lon_sw, calculation): + def __init__(self, auth, lat_ne, lon_ne, lat_sw, lon_sw): """Initialize the data object.""" self.auth = auth self.data = None @@ -116,26 +166,20 @@ class NetatmoPublicData: self.lon_ne = lon_ne self.lat_sw = lat_sw self.lon_sw = lon_sw - self.calculation = calculation @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Request an update from the Netatmo API.""" import pyatmo - raindata = pyatmo.PublicData(self.auth, - LAT_NE=self.lat_ne, - LON_NE=self.lon_ne, - LAT_SW=self.lat_sw, - LON_SW=self.lon_sw, - required_data_type="rain") + data = pyatmo.PublicData(self.auth, + LAT_NE=self.lat_ne, + LON_NE=self.lon_ne, + LAT_SW=self.lat_sw, + LON_SW=self.lon_sw, + filtering=True) - if raindata.CountStationInArea() == 0: - _LOGGER.warning('No Rain Station available in this area.') + if data.CountStationInArea() == 0: + _LOGGER.warning('No Stations available in this area.') return - raindata_live = raindata.getLive() - - if self.calculation == 'avg': - self.data = sum(raindata_live.values()) / len(raindata_live) - else: - self.data = max(raindata_live.values()) + self.data = data diff --git a/requirements_all.txt b/requirements_all.txt index e009f2e21a0..cfb17e64a7a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -794,7 +794,7 @@ pyasn1-modules==0.1.5 pyasn1==0.3.7 # homeassistant.components.netatmo -pyatmo==1.1.1 +pyatmo==1.2 # homeassistant.components.apple_tv pyatv==0.3.10 From 4a4c07ac1b209471ebb3467c7a884c29e2f5edd6 Mon Sep 17 00:00:00 2001 From: Daniel Winks Date: Tue, 25 Sep 2018 05:18:23 -0400 Subject: [PATCH 032/247] GitLab-CI sensor integration addition. (#16561) * Updates to GitLab Sensor * Updates to GitLab_CI sensor. * Added GitLab_CI sensor. * Updated interval to a more appropriate 300 seconds. * Added GitLab_CI.py to ommitted files. * Initial refactor to use python-gitlab PyPI module. * Fixes to dict parsing. * Updated required packages for GitLab_CI requirements. * Updates and refactoring to more closely align with Home-Assistant standards. * Moved import to init, removed unreachable requests exception. * Removed references to STATE_UNKNOWN and replaced with None * Removed extra whitespace * Changed PLATFORM_SCHEMA and renamed add_devices. Changed PLATFORM_SCHEMA to use SCAN_INTERVAL and add_devices to add_entities * Added configurable name, changed logger and removed cruft. * Removed _status to use _state instead, as both held same value. * Fixed ATTR_BUILD_BRANCH, removed more extraneous cruft. * Changed required config keys to dict[key] format. * Removed extraneous CONF_SCAN_INTERVAL as it's already defined. --- .coveragerc | 1 + homeassistant/components/sensor/gitlab_ci.py | 174 +++++++++++++++++++ requirements_all.txt | 3 + 3 files changed, 178 insertions(+) create mode 100644 homeassistant/components/sensor/gitlab_ci.py diff --git a/.coveragerc b/.coveragerc index a8d7d89544d..defbc0022bd 100644 --- a/.coveragerc +++ b/.coveragerc @@ -679,6 +679,7 @@ omit = homeassistant/components/sensor/fritzbox_netmonitor.py homeassistant/components/sensor/gearbest.py homeassistant/components/sensor/geizhals.py + homeassistant/components/sensor/gitlab_ci.py homeassistant/components/sensor/gitter.py homeassistant/components/sensor/glances.py homeassistant/components/sensor/google_travel_time.py diff --git a/homeassistant/components/sensor/gitlab_ci.py b/homeassistant/components/sensor/gitlab_ci.py new file mode 100644 index 00000000000..ceb5f75cace --- /dev/null +++ b/homeassistant/components/sensor/gitlab_ci.py @@ -0,0 +1,174 @@ +"""Module for retrieving latest GitLab CI job information.""" +from datetime import timedelta +import logging + +import voluptuous as vol + +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import ( + ATTR_ATTRIBUTION, CONF_NAME, CONF_SCAN_INTERVAL, CONF_TOKEN, CONF_URL) +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity +from homeassistant.util import Throttle + +CONF_GITLAB_ID = 'gitlab_id' +CONF_ATTRIBUTION = "Information provided by https://gitlab.com/" + +ICON_HAPPY = 'mdi:emoticon-happy' +ICON_SAD = 'mdi:emoticon-happy' +ICON_OTHER = 'mdi:git' + +ATTR_BUILD_ID = 'build id' +ATTR_BUILD_STATUS = 'build_status' +ATTR_BUILD_STARTED = 'build_started' +ATTR_BUILD_FINISHED = 'build_finished' +ATTR_BUILD_DURATION = 'build_duration' +ATTR_BUILD_COMMIT_ID = 'commit id' +ATTR_BUILD_COMMIT_DATE = 'commit date' +ATTR_BUILD_BRANCH = 'build branch' + +SCAN_INTERVAL = timedelta(seconds=300) + +_LOGGER = logging.getLogger(__name__) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_TOKEN): cv.string, + vol.Required(CONF_GITLAB_ID): cv.string, + vol.Optional(CONF_NAME, default='GitLab CI Status'): cv.string, + vol.Optional(CONF_URL, default='https://gitlab.com'): cv.string +}) + +REQUIREMENTS = ['python-gitlab==1.6.0'] + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Sensor platform setup.""" + _name = config.get(CONF_NAME) + _interval = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL) + _url = config.get(CONF_URL) + + _gitlab_data = GitLabData( + priv_token=config[CONF_TOKEN], + gitlab_id=config[CONF_GITLAB_ID], + interval=_interval, + url=_url + ) + + add_entities([GitLabSensor(_gitlab_data, _name)], True) + + +class GitLabSensor(Entity): + """Representation of a Sensor.""" + + def __init__(self, gitlab_data, name): + """Initialize the sensor.""" + self._available = False + self._state = None + self._started_at = None + self._finished_at = None + self._duration = None + self._commit_id = None + self._commit_date = None + self._build_id = None + self._branch = None + self._gitlab_data = gitlab_data + self._name = name + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + @property + def available(self): + """Return True if entity is available.""" + return self._available + + @property + def device_state_attributes(self): + """Return the state attributes.""" + return { + ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + ATTR_BUILD_STATUS: self._state, + ATTR_BUILD_STARTED: self._started_at, + ATTR_BUILD_FINISHED: self._finished_at, + ATTR_BUILD_DURATION: self._duration, + ATTR_BUILD_COMMIT_ID: self._commit_id, + ATTR_BUILD_COMMIT_DATE: self._commit_date, + ATTR_BUILD_ID: self._build_id, + ATTR_BUILD_BRANCH: self._branch + } + + @property + def icon(self): + """Return the icon to use in the frontend.""" + if self._state == 'success': + return ICON_HAPPY + if self._state == 'failed': + return ICON_SAD + return ICON_OTHER + + def update(self): + """Collect updated data from GitLab API.""" + self._gitlab_data.update() + + self._state = self._gitlab_data.status + self._started_at = self._gitlab_data.started_at + self._finished_at = self._gitlab_data.finished_at + self._duration = self._gitlab_data.duration + self._commit_id = self._gitlab_data.commit_id + self._commit_date = self._gitlab_data.commit_date + self._build_id = self._gitlab_data.build_id + self._branch = self._gitlab_data.branch + self._available = self._gitlab_data.available + + +class GitLabData(): + """GitLab Data object.""" + + def __init__(self, gitlab_id, priv_token, interval, url): + """Fetch data from GitLab API for most recent CI job.""" + import gitlab + self._gitlab_id = gitlab_id + self._gitlab = gitlab.Gitlab( + url, private_token=priv_token, per_page=1) + self._gitlab.auth() + self._gitlab_exceptions = gitlab.exceptions + self.update = Throttle(interval)(self._update) + + self.available = False + self.status = None + self.started_at = None + self.finished_at = None + self.duration = None + self.commit_id = None + self.commit_date = None + self.build_id = None + self.branch = None + + def _update(self): + try: + _projects = self._gitlab.projects.get(self._gitlab_id) + _last_pipeline = _projects.pipelines.list(page=1)[0] + _last_job = _last_pipeline.jobs.list(page=1)[0] + self.status = _last_pipeline.attributes.get('status') + self.started_at = _last_job.attributes.get('started_at') + self.finished_at = _last_job.attributes.get('finished_at') + self.duration = _last_job.attributes.get('duration') + _commit = _last_job.attributes.get('commit') + self.commit_id = _commit.get('id') + self.commit_date = _commit.get('committed_date') + self.build_id = _last_job.attributes.get('id') + self.branch = _last_job.attributes.get('ref') + self.available = True + except self._gitlab_exceptions.GitlabAuthenticationError as erra: + _LOGGER.error("Authentication Error: %s", erra) + self.available = False + except self._gitlab_exceptions.GitlabGetError as errg: + _LOGGER.error("Project Not Found: %s", errg) + self.available = False diff --git a/requirements_all.txt b/requirements_all.txt index cfb17e64a7a..40d4442dee2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1114,6 +1114,9 @@ python-forecastio==1.4.0 # homeassistant.components.gc100 python-gc100==1.0.3a +# homeassistant.components.sensor.gitlab_ci +python-gitlab==1.6.0 + # homeassistant.components.sensor.hp_ilo python-hpilo==3.9 From 2b2502c91c8fbd1f35c0f1654e5fb7776c8cc5b7 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 25 Sep 2018 11:57:32 +0200 Subject: [PATCH 033/247] Support old tradfri config format (#16841) --- .../components/tradfri/config_flow.py | 4 ++- tests/components/tradfri/test_config_flow.py | 31 +++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/tradfri/config_flow.py b/homeassistant/components/tradfri/config_flow.py index 4de43c79e0c..8d8f9af79e6 100644 --- a/homeassistant/components/tradfri/config_flow.py +++ b/homeassistant/components/tradfri/config_flow.py @@ -95,7 +95,9 @@ class FlowHandler(config_entries.ConfigFlow): try: data = await get_gateway_info( - self.hass, user_input['host'], user_input['identity'], + self.hass, user_input['host'], + # Old config format had a fixed identity + user_input.get('identity', 'homeassistant'), user_input['key']) data[CONF_IMPORT_GROUPS] = user_input[CONF_IMPORT_GROUPS] diff --git a/tests/components/tradfri/test_config_flow.py b/tests/components/tradfri/test_config_flow.py index 4650fb5d9bc..580e9580d76 100644 --- a/tests/components/tradfri/test_config_flow.py +++ b/tests/components/tradfri/test_config_flow.py @@ -154,3 +154,34 @@ async def test_import_connection(hass, mock_gateway_info, mock_entry_setup): assert len(mock_gateway_info.mock_calls) == 1 assert len(mock_entry_setup.mock_calls) == 1 + + +async def test_import_connection_legacy(hass, mock_gateway_info, + mock_entry_setup): + """Test a connection via import.""" + mock_gateway_info.side_effect = \ + lambda hass, host, identity, key: mock_coro({ + 'host': host, + 'identity': identity, + 'key': key, + 'gateway_id': 'mock-gateway' + }) + + result = await hass.config_entries.flow.async_init( + 'tradfri', context={'source': 'import'}, data={ + 'host': '123.123.123.123', + 'key': 'mock-key', + 'import_groups': True + }) + + assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result['result'].data == { + 'host': '123.123.123.123', + 'gateway_id': 'mock-gateway', + 'identity': 'homeassistant', + 'key': 'mock-key', + 'import_groups': True + } + + assert len(mock_gateway_info.mock_calls) == 1 + assert len(mock_entry_setup.mock_calls) == 1 From f4974f58fee79d5093bccacc118d19a35f9cbba9 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 25 Sep 2018 12:21:11 +0200 Subject: [PATCH 034/247] Config entry update data (#16843) * WIP * Allow updating data of a config entry --- homeassistant/config_entries.py | 6 ++++++ tests/test_config_entries.py | 17 +++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 7763594e0e1..a2a9ce78989 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -379,6 +379,12 @@ class ConfigEntries: CONN_CLASS_UNKNOWN)) for entry in config['entries']] + @callback + def async_update_entry(self, entry, *, data): + """Update a config entry.""" + entry.data = data + self._async_schedule_save() + async def async_forward_entry_setup(self, entry, component): """Forward the setup of an entry to a different component. diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 57d63eb8271..afb13d71c2e 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -315,3 +315,20 @@ async def test_loading_default_config(hass): await manager.async_load() assert len(manager.async_entries()) == 0 + + +async def test_updating_entry_data(manager): + """Test that we can update an entry data.""" + entry = MockConfigEntry( + domain='test', + data={'first': True}, + ) + entry.add_to_manager(manager) + + manager.async_update_entry(entry, data={ + 'second': True + }) + + assert entry.data == { + 'second': True + } From e4898bb05c88b94d1a11008e5a323486055898ad Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 25 Sep 2018 12:22:14 +0200 Subject: [PATCH 035/247] Allow MQTT discovery (#16842) --- .../components/mqtt/.translations/en.json | 1 + homeassistant/components/mqtt/__init__.py | 35 ++++++++++--------- homeassistant/components/mqtt/config_flow.py | 3 +- homeassistant/components/mqtt/const.py | 2 ++ homeassistant/components/mqtt/strings.json | 3 +- tests/components/mqtt/test_config_flow.py | 5 +++ 6 files changed, 31 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/mqtt/.translations/en.json b/homeassistant/components/mqtt/.translations/en.json index 1f0ed341bb6..c0b83a1323f 100644 --- a/homeassistant/components/mqtt/.translations/en.json +++ b/homeassistant/components/mqtt/.translations/en.json @@ -10,6 +10,7 @@ "broker": { "data": { "broker": "Broker", + "discovery": "Enable discovery", "password": "Password", "port": "Port", "username": "Username" diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index abc240a65cb..856d5d01894 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -36,7 +36,7 @@ from homeassistant.util.async_ import ( # Loading the config flow file will register the flow from . import config_flow # noqa # pylint: disable=unused-import -from .const import CONF_BROKER +from .const import CONF_BROKER, CONF_DISCOVERY, DEFAULT_DISCOVERY from .server import HBMQTT_CONFIG_SCHEMA REQUIREMENTS = ['paho-mqtt==1.4.0'] @@ -47,13 +47,13 @@ DOMAIN = 'mqtt' DATA_MQTT = 'mqtt' DATA_MQTT_CONFIG = 'mqtt_config' +DATA_MQTT_HASS_CONFIG = 'mqtt_hass_config' SERVICE_PUBLISH = 'publish' CONF_EMBEDDED = 'embedded' CONF_CLIENT_ID = 'client_id' -CONF_DISCOVERY = 'discovery' CONF_DISCOVERY_PREFIX = 'discovery_prefix' CONF_KEEPALIVE = 'keepalive' CONF_CERTIFICATE = 'certificate' @@ -81,7 +81,6 @@ DEFAULT_KEEPALIVE = 60 DEFAULT_QOS = 0 DEFAULT_RETAIN = False DEFAULT_PROTOCOL = PROTOCOL_311 -DEFAULT_DISCOVERY = False DEFAULT_DISCOVERY_PREFIX = 'homeassistant' DEFAULT_TLS_PROTOCOL = 'auto' DEFAULT_PAYLOAD_AVAILABLE = 'online' @@ -321,23 +320,21 @@ async def _async_setup_server(hass: HomeAssistantType, config: ConfigType): return broker_config -async def _async_setup_discovery(hass: HomeAssistantType, - config: ConfigType) -> bool: +async def _async_setup_discovery(hass: HomeAssistantType, conf: ConfigType, + hass_config: ConfigType) -> bool: """Try to start the discovery of MQTT devices. This method is a coroutine. """ - conf = config.get(DOMAIN, {}) # type: ConfigType - discovery = await async_prepare_setup_platform( - hass, config, DOMAIN, 'discovery') + hass, hass_config, DOMAIN, 'discovery') if discovery is None: _LOGGER.error("Unable to load MQTT discovery") return False success = await discovery.async_start( - hass, conf[CONF_DISCOVERY_PREFIX], config) # type: bool + hass, conf[CONF_DISCOVERY_PREFIX], hass_config) # type: bool return success @@ -346,6 +343,11 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: """Start the MQTT protocol service.""" conf = config.get(DOMAIN) # type: Optional[ConfigType] + # We need this because discovery can cause components to be set up and + # otherwise it will not load the users config. + # This needs a better solution. + hass.data[DATA_MQTT_HASS_CONFIG] = config + if conf is None: # If we have a config entry, setup is done by that config entry. # If there is no config entry, this should fail. @@ -390,13 +392,6 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: data={} )) - if conf.get(CONF_DISCOVERY): - async def async_setup_discovery(event): - await _async_setup_discovery(hass, config) - - hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_START, async_setup_discovery) - return True @@ -528,6 +523,14 @@ async def async_setup_entry(hass, entry): DOMAIN, SERVICE_PUBLISH, async_publish_service, schema=MQTT_PUBLISH_SCHEMA) + if conf.get(CONF_DISCOVERY): + async def async_setup_discovery(event): + await _async_setup_discovery( + hass, conf, hass.data[DATA_MQTT_HASS_CONFIG]) + + hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_START, async_setup_discovery) + return True diff --git a/homeassistant/components/mqtt/config_flow.py b/homeassistant/components/mqtt/config_flow.py index a8987a19742..22072857b03 100644 --- a/homeassistant/components/mqtt/config_flow.py +++ b/homeassistant/components/mqtt/config_flow.py @@ -7,7 +7,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_PASSWORD, CONF_PORT, CONF_USERNAME -from .const import CONF_BROKER +from .const import CONF_BROKER, CONF_DISCOVERY, DEFAULT_DISCOVERY @config_entries.HANDLERS.register('mqtt') @@ -44,6 +44,7 @@ class FlowHandler(config_entries.ConfigFlow): fields[vol.Required(CONF_PORT, default=1883)] = vol.Coerce(int) fields[vol.Optional(CONF_USERNAME)] = str fields[vol.Optional(CONF_PASSWORD)] = str + fields[vol.Optional(CONF_DISCOVERY, default=DEFAULT_DISCOVERY)] = bool return self.async_show_form( step_id='broker', data_schema=vol.Schema(fields), errors=errors) diff --git a/homeassistant/components/mqtt/const.py b/homeassistant/components/mqtt/const.py index 8f9d938cf88..3c22001f91c 100644 --- a/homeassistant/components/mqtt/const.py +++ b/homeassistant/components/mqtt/const.py @@ -1,2 +1,4 @@ """Constants used by multiple MQTT modules.""" CONF_BROKER = 'broker' +CONF_DISCOVERY = 'discovery' +DEFAULT_DISCOVERY = False diff --git a/homeassistant/components/mqtt/strings.json b/homeassistant/components/mqtt/strings.json index a38983125ae..0a2cb255cc4 100644 --- a/homeassistant/components/mqtt/strings.json +++ b/homeassistant/components/mqtt/strings.json @@ -9,7 +9,8 @@ "broker": "Broker", "port": "Port", "username": "Username", - "password": "Password" + "password": "Password", + "discovery": "Enable discovery" } } }, diff --git a/tests/components/mqtt/test_config_flow.py b/tests/components/mqtt/test_config_flow.py index 4a4d783940f..9f6be60c68b 100644 --- a/tests/components/mqtt/test_config_flow.py +++ b/tests/components/mqtt/test_config_flow.py @@ -41,6 +41,11 @@ async def test_user_connection_works(hass, mock_try_connection, ) assert result['type'] == 'create_entry' + assert result['result'].data == { + 'broker': '127.0.0.1', + 'port': 1883, + 'discovery': False, + } # Check we tried the connection assert len(mock_try_connection.mock_calls) == 1 # Check config entry got setup From 7840b1e387ce17512619317a52d9ab0b41737e73 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 25 Sep 2018 12:22:27 +0200 Subject: [PATCH 036/247] Fix MQTT leaving files behind (#16840) --- tests/components/binary_sensor/test_mqtt.py | 47 ++++++++++--------- tests/components/switch/test_mqtt.py | 50 +++++++++++---------- 2 files changed, 52 insertions(+), 45 deletions(-) diff --git a/tests/components/binary_sensor/test_mqtt.py b/tests/components/binary_sensor/test_mqtt.py index 57050c2cbf5..0bc8c6c81fb 100644 --- a/tests/components/binary_sensor/test_mqtt.py +++ b/tests/components/binary_sensor/test_mqtt.py @@ -2,14 +2,15 @@ import unittest import homeassistant.core as ha -from homeassistant.setup import setup_component +from homeassistant.setup import setup_component, async_setup_component import homeassistant.components.binary_sensor as binary_sensor from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.const import EVENT_STATE_CHANGED, STATE_UNAVAILABLE -from tests.common import get_test_home_assistant, fire_mqtt_message -from tests.common import mock_component, mock_mqtt_component +from tests.common import ( + get_test_home_assistant, fire_mqtt_message, async_fire_mqtt_message, + mock_component, mock_mqtt_component, async_mock_mqtt_component) class TestSensorMQTT(unittest.TestCase): @@ -77,25 +78,6 @@ class TestSensorMQTT(unittest.TestCase): state = self.hass.states.get('binary_sensor.test') self.assertIsNone(state) - def test_unique_id(self): - """Test unique id option only creates one sensor per unique_id.""" - assert setup_component(self.hass, binary_sensor.DOMAIN, { - binary_sensor.DOMAIN: [{ - 'platform': 'mqtt', - 'name': 'Test 1', - 'state_topic': 'test-topic', - 'unique_id': 'TOTALLY_UNIQUE' - }, { - 'platform': 'mqtt', - 'name': 'Test 2', - 'state_topic': 'test-topic', - 'unique_id': 'TOTALLY_UNIQUE' - }] - }) - fire_mqtt_message(self.hass, 'test-topic', 'payload') - self.hass.block_till_done() - assert len(self.hass.states.all()) == 1 - def test_availability_without_topic(self): """Test availability without defined availability topic.""" self.assertTrue(setup_component(self.hass, binary_sensor.DOMAIN, { @@ -223,3 +205,24 @@ class TestSensorMQTT(unittest.TestCase): fire_mqtt_message(self.hass, 'test-topic', 'ON') self.hass.block_till_done() self.assertEqual(2, len(events)) + + +async def test_unique_id(hass): + """Test unique id option only creates one sensor per unique_id.""" + await async_mock_mqtt_component(hass) + assert await async_setup_component(hass, binary_sensor.DOMAIN, { + binary_sensor.DOMAIN: [{ + 'platform': 'mqtt', + 'name': 'Test 1', + 'state_topic': 'test-topic', + 'unique_id': 'TOTALLY_UNIQUE' + }, { + 'platform': 'mqtt', + 'name': 'Test 2', + 'state_topic': 'test-topic', + 'unique_id': 'TOTALLY_UNIQUE' + }] + }) + async_fire_mqtt_message(hass, 'test-topic', 'payload') + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 1 diff --git a/tests/components/switch/test_mqtt.py b/tests/components/switch/test_mqtt.py index c9bfd02156f..321db7ed118 100644 --- a/tests/components/switch/test_mqtt.py +++ b/tests/components/switch/test_mqtt.py @@ -2,13 +2,14 @@ import unittest from unittest.mock import patch -from homeassistant.setup import setup_component +from homeassistant.setup import setup_component, async_setup_component from homeassistant.const import STATE_ON, STATE_OFF, STATE_UNAVAILABLE,\ ATTR_ASSUMED_STATE import homeassistant.core as ha import homeassistant.components.switch as switch from tests.common import ( - mock_mqtt_component, fire_mqtt_message, get_test_home_assistant, mock_coro) + mock_mqtt_component, fire_mqtt_message, get_test_home_assistant, mock_coro, + async_mock_mqtt_component, async_fire_mqtt_message) class TestSwitchMQTT(unittest.TestCase): @@ -280,25 +281,28 @@ class TestSwitchMQTT(unittest.TestCase): state = self.hass.states.get('switch.test') self.assertEqual(STATE_OFF, state.state) - def test_unique_id(self): - """Test unique id option only creates one switch per unique_id.""" - assert setup_component(self.hass, switch.DOMAIN, { - switch.DOMAIN: [{ - 'platform': 'mqtt', - 'name': 'Test 1', - 'state_topic': 'test-topic', - 'command_topic': 'command-topic', - 'unique_id': 'TOTALLY_UNIQUE' - }, { - 'platform': 'mqtt', - 'name': 'Test 2', - 'state_topic': 'test-topic', - 'command_topic': 'command-topic', - 'unique_id': 'TOTALLY_UNIQUE' - }] - }) - fire_mqtt_message(self.hass, 'test-topic', 'payload') - self.hass.block_till_done() - assert len(self.hass.states.async_entity_ids()) == 2 - # all switches group is 1, unique id created is 1 +async def test_unique_id(hass): + """Test unique id option only creates one switch per unique_id.""" + await async_mock_mqtt_component(hass) + assert await async_setup_component(hass, switch.DOMAIN, { + switch.DOMAIN: [{ + 'platform': 'mqtt', + 'name': 'Test 1', + 'state_topic': 'test-topic', + 'command_topic': 'command-topic', + 'unique_id': 'TOTALLY_UNIQUE' + }, { + 'platform': 'mqtt', + 'name': 'Test 2', + 'state_topic': 'test-topic', + 'command_topic': 'command-topic', + 'unique_id': 'TOTALLY_UNIQUE' + }] + }) + + async_fire_mqtt_message(hass, 'test-topic', 'payload') + await hass.async_block_till_done() + + assert len(hass.states.async_entity_ids()) == 2 + # all switches group is 1, unique id created is 1 From 01925fdfffe68dbcc1ee04348c6408bf903122f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Tue, 25 Sep 2018 13:41:29 +0200 Subject: [PATCH 037/247] change unknown to None in Netatmo public (#16845) --- homeassistant/components/sensor/netatmo_public.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sensor/netatmo_public.py b/homeassistant/components/sensor/netatmo_public.py index 59b0b317ed1..7a500b66183 100644 --- a/homeassistant/components/sensor/netatmo_public.py +++ b/homeassistant/components/sensor/netatmo_public.py @@ -12,7 +12,7 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( CONF_NAME, CONF_MODE, CONF_MONITORED_CONDITIONS, TEMP_CELSIUS, - DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_HUMIDITY, STATE_UNKNOWN) + DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_HUMIDITY) from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle @@ -125,7 +125,7 @@ class NetatmoPublicSensor(Entity): if self.netatmo_data.data is None: _LOGGER.warning("No data found for %s", self._name) - self._state = STATE_UNKNOWN + self._state = None return data = None @@ -146,7 +146,7 @@ class NetatmoPublicSensor(Entity): if not data: _LOGGER.warning("No station provides %s data in the area %s", self.type, self._area_name) - self._state = STATE_UNKNOWN + self._state = None return if self._mode == 'avg': From 9ea5afd109a8fc0b4aed57e45dde67a4245cb905 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 25 Sep 2018 13:47:12 +0200 Subject: [PATCH 038/247] Add unique ID and device info to Nest camera (#16846) * Add unique ID and device info to Nest camera * Remove sw version --- homeassistant/components/camera/nest.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/homeassistant/components/camera/nest.py b/homeassistant/components/camera/nest.py index e1d26371984..158123989c0 100644 --- a/homeassistant/components/camera/nest.py +++ b/homeassistant/components/camera/nest.py @@ -62,6 +62,23 @@ class NestCamera(Camera): """Return the name of the nest, if any.""" return self._name + @property + def unique_id(self): + """Return the serial number.""" + return self.device.device_id + + @property + def device_info(self): + """Return information about the device.""" + return { + 'identifiers': { + (nest.DOMAIN, self.device.device_id) + }, + 'name': self.device.name_long, + 'manufacturer': 'Nest Labs', + 'model': "Camera", + } + @property def should_poll(self): """Nest camera should poll periodically.""" From 093285f92ff6036034c5dc415f1b7b23329ac273 Mon Sep 17 00:00:00 2001 From: emontnemery Date: Tue, 25 Sep 2018 14:25:03 +0200 Subject: [PATCH 039/247] Remove discovered MQTT binary_sensor device when discovery topic is cleared (#16826) --- .../components/binary_sensor/mqtt.py | 24 +++++++++++++------ tests/components/mqtt/test_discovery.py | 22 +++++++++++++++++ 2 files changed, 39 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/binary_sensor/mqtt.py b/homeassistant/components/binary_sensor/mqtt.py index 37a26a27214..ca2ff2074c3 100644 --- a/homeassistant/components/binary_sensor/mqtt.py +++ b/homeassistant/components/binary_sensor/mqtt.py @@ -18,8 +18,9 @@ from homeassistant.const import ( CONF_FORCE_UPDATE, CONF_NAME, CONF_VALUE_TEMPLATE, CONF_PAYLOAD_ON, CONF_PAYLOAD_OFF, CONF_DEVICE_CLASS) from homeassistant.components.mqtt import ( - CONF_STATE_TOPIC, CONF_AVAILABILITY_TOPIC, CONF_PAYLOAD_AVAILABLE, - CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, MqttAvailability) + ATTR_DISCOVERY_HASH, CONF_STATE_TOPIC, CONF_AVAILABILITY_TOPIC, + CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, + MqttAvailability, MqttDiscoveryUpdate) import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -55,6 +56,10 @@ def async_setup_platform(hass, config, async_add_entities, if value_template is not None: value_template.hass = hass + discovery_hash = None + if discovery_info is not None and ATTR_DISCOVERY_HASH in discovery_info: + discovery_hash = discovery_info[ATTR_DISCOVERY_HASH] + async_add_entities([MqttBinarySensor( config.get(CONF_NAME), config.get(CONF_STATE_TOPIC), @@ -68,19 +73,22 @@ def async_setup_platform(hass, config, async_add_entities, config.get(CONF_PAYLOAD_NOT_AVAILABLE), value_template, config.get(CONF_UNIQUE_ID), + discovery_hash, )]) -class MqttBinarySensor(MqttAvailability, BinarySensorDevice): +class MqttBinarySensor(MqttAvailability, MqttDiscoveryUpdate, + BinarySensorDevice): """Representation a binary sensor that is updated by MQTT.""" def __init__(self, name, state_topic, availability_topic, device_class, qos, force_update, payload_on, payload_off, payload_available, payload_not_available, value_template, - unique_id: Optional[str]): + unique_id: Optional[str], discovery_hash): """Initialize the MQTT binary sensor.""" - super().__init__(availability_topic, qos, payload_available, - payload_not_available) + MqttAvailability.__init__(self, availability_topic, qos, + payload_available, payload_not_available) + MqttDiscoveryUpdate.__init__(self, discovery_hash) self._name = name self._state = None self._state_topic = state_topic @@ -91,11 +99,13 @@ class MqttBinarySensor(MqttAvailability, BinarySensorDevice): self._force_update = force_update self._template = value_template self._unique_id = unique_id + self._discovery_hash = discovery_hash @asyncio.coroutine def async_added_to_hass(self): """Subscribe mqtt events.""" - yield from super().async_added_to_hass() + yield from MqttAvailability.async_added_to_hass(self) + yield from MqttDiscoveryUpdate.async_added_to_hass(self) @callback def state_message_received(topic, payload, qos): diff --git a/tests/components/mqtt/test_discovery.py b/tests/components/mqtt/test_discovery.py index 9e334719a36..0656bbdc300 100644 --- a/tests/components/mqtt/test_discovery.py +++ b/tests/components/mqtt/test_discovery.py @@ -211,6 +211,28 @@ def test_discovery_removal(hass, mqtt_mock, caplog): assert state is None +@asyncio.coroutine +def test_discovery_removal_binary_sensor(hass, mqtt_mock, caplog): + """Test removal of discovered binary_sensor.""" + yield from async_start(hass, 'homeassistant', {}) + data = ( + '{ "name": "Beer",' + ' "status_topic": "test_topic" }' + ) + async_fire_mqtt_message(hass, 'homeassistant/binary_sensor/bla/config', + data) + yield from hass.async_block_till_done() + state = hass.states.get('binary_sensor.beer') + assert state is not None + assert state.name == 'Beer' + async_fire_mqtt_message(hass, 'homeassistant/binary_sensor/bla/config', + '') + yield from hass.async_block_till_done() + yield from hass.async_block_till_done() + state = hass.states.get('binary_sensor.beer') + assert state is None + + @asyncio.coroutine def test_discovery_removal_light(hass, mqtt_mock, caplog): """Test removal of discovered light.""" From a1c914dfeb7c1e45b00dd9c49a27e455eefca368 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 25 Sep 2018 14:29:13 +0200 Subject: [PATCH 040/247] On removal, only unload config entry if loaded (#16844) * On removal, only unload config entry if loaded * Fix test --- homeassistant/config_entries.py | 5 ++- .../components/config/test_config_entries.py | 2 +- tests/test_config_entries.py | 39 ++++++++++++++++++- 3 files changed, 42 insertions(+), 4 deletions(-) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index a2a9ce78989..83bf9d22de3 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -340,7 +340,10 @@ class ConfigEntries: entry = self._entries.pop(found) self._async_schedule_save() - unloaded = await entry.async_unload(self.hass) + if entry.state == ENTRY_STATE_LOADED: + unloaded = await entry.async_unload(self.hass) + else: + unloaded = True device_registry = await \ self.hass.helpers.device_registry.async_get_registry() diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index 66d29aac757..1e3b507727c 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -72,7 +72,7 @@ def test_get_entries(hass, client): @asyncio.coroutine def test_remove_entry(hass, client): """Test removing an entry via the API.""" - entry = MockConfigEntry(domain='demo') + entry = MockConfigEntry(domain='demo', state=core_ce.ENTRY_STATE_LOADED) entry.add_to_hass(hass) resp = yield from client.delete( '/api/config/config_entries/entry/{}'.format(entry.entry_id)) diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index afb13d71c2e..8f12407c6b7 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -49,7 +49,11 @@ def test_remove_entry(hass, manager): MockModule('comp', async_unload_entry=mock_unload_entry)) MockConfigEntry(domain='test', entry_id='test1').add_to_manager(manager) - MockConfigEntry(domain='test', entry_id='test2').add_to_manager(manager) + MockConfigEntry( + domain='test', + entry_id='test2', + state=config_entries.ENTRY_STATE_LOADED + ).add_to_manager(manager) MockConfigEntry(domain='test', entry_id='test3').add_to_manager(manager) assert [item.entry_id for item in manager.async_entries()] == \ @@ -79,7 +83,11 @@ def test_remove_entry_raises(hass, manager): MockModule('comp', async_unload_entry=mock_unload_entry)) MockConfigEntry(domain='test', entry_id='test1').add_to_manager(manager) - MockConfigEntry(domain='test', entry_id='test2').add_to_manager(manager) + MockConfigEntry( + domain='test', + entry_id='test2', + state=config_entries.ENTRY_STATE_LOADED + ).add_to_manager(manager) MockConfigEntry(domain='test', entry_id='test3').add_to_manager(manager) assert [item.entry_id for item in manager.async_entries()] == \ @@ -94,6 +102,33 @@ def test_remove_entry_raises(hass, manager): ['test1', 'test3'] +@asyncio.coroutine +def test_remove_entry_if_not_loaded(hass, manager): + """Test that we can remove an entry.""" + mock_unload_entry = MagicMock(return_value=mock_coro(True)) + + loader.set_component( + hass, 'test', + MockModule('comp', async_unload_entry=mock_unload_entry)) + + MockConfigEntry(domain='test', entry_id='test1').add_to_manager(manager) + MockConfigEntry(domain='test', entry_id='test2').add_to_manager(manager) + MockConfigEntry(domain='test', entry_id='test3').add_to_manager(manager) + + assert [item.entry_id for item in manager.async_entries()] == \ + ['test1', 'test2', 'test3'] + + result = yield from manager.async_remove('test2') + + assert result == { + 'require_restart': False + } + assert [item.entry_id for item in manager.async_entries()] == \ + ['test1', 'test3'] + + assert len(mock_unload_entry.mock_calls) == 0 + + @asyncio.coroutine def test_add_entry_calls_setup_entry(hass, manager): """Test we call setup_config_entry.""" From bc8d323bdd6a66fba254d20b0339603fe6bd63ec Mon Sep 17 00:00:00 2001 From: Tommy Jonsson Date: Tue, 25 Sep 2018 15:04:43 +0200 Subject: [PATCH 041/247] Add image support to hangouts notifications (#16560) * add image to services.yaml * add image support Add image support to hangouts notification. * fix indents * fix line length * Add data schema forgot schema * fix linelength * use is_allowed_path and better file error handling * elif not else if * fix logger error * fixes * fix travis errors/warnings * remove trailing whitespace * fix travis pylint naming * new async style * removed unused async_timeout * change to image_file/url (#3) * change to image_file/url * removed whitespace * forgot to remove unused urlparse import * image_file/url in service help --- homeassistant/components/hangouts/const.py | 10 +++- .../components/hangouts/hangouts_bot.py | 55 +++++++++++++++++-- .../components/hangouts/services.yaml | 6 +- 3 files changed, 62 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/hangouts/const.py b/homeassistant/components/hangouts/const.py index caae0de169b..5a527fae260 100644 --- a/homeassistant/components/hangouts/const.py +++ b/homeassistant/components/hangouts/const.py @@ -3,7 +3,8 @@ import logging import voluptuous as vol -from homeassistant.components.notify import ATTR_MESSAGE, ATTR_TARGET +from homeassistant.components.notify \ + import ATTR_MESSAGE, ATTR_TARGET, ATTR_DATA import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger('homeassistant.components.hangouts') @@ -56,10 +57,15 @@ MESSAGE_SEGMENT_SCHEMA = vol.Schema({ vol.Optional('parse_str'): cv.boolean, vol.Optional('link_target'): cv.string }) +MESSAGE_DATA_SCHEMA = vol.Schema({ + vol.Optional('image_file'): cv.string, + vol.Optional('image_url'): cv.string +}) MESSAGE_SCHEMA = vol.Schema({ vol.Required(ATTR_TARGET): [TARGETS_SCHEMA], - vol.Required(ATTR_MESSAGE): [MESSAGE_SEGMENT_SCHEMA] + vol.Required(ATTR_MESSAGE): [MESSAGE_SEGMENT_SCHEMA], + vol.Optional(ATTR_DATA): MESSAGE_DATA_SCHEMA }) INTENT_SCHEMA = vol.All( diff --git a/homeassistant/components/hangouts/hangouts_bot.py b/homeassistant/components/hangouts/hangouts_bot.py index 7edc8898c8c..8747bff9ba7 100644 --- a/homeassistant/components/hangouts/hangouts_bot.py +++ b/homeassistant/components/hangouts/hangouts_bot.py @@ -1,10 +1,13 @@ """The Hangouts Bot.""" +import io import logging - +import asyncio +import aiohttp +from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers import dispatcher, intent from .const import ( - ATTR_MESSAGE, ATTR_TARGET, CONF_CONVERSATIONS, DOMAIN, + ATTR_MESSAGE, ATTR_TARGET, ATTR_DATA, CONF_CONVERSATIONS, DOMAIN, EVENT_HANGOUTS_CONNECTED, EVENT_HANGOUTS_CONVERSATIONS_CHANGED, EVENT_HANGOUTS_DISCONNECTED, EVENT_HANGOUTS_MESSAGE_RECEIVED, CONF_MATCHERS, CONF_CONVERSATION_ID, @@ -146,7 +149,8 @@ class HangoutsBot: is_error and conv_id in self._error_suppressed_conv_ids): await self._async_send_message( [{'text': message, 'parse_str': True}], - [{CONF_CONVERSATION_ID: conv_id}]) + [{CONF_CONVERSATION_ID: conv_id}], + None) async def _async_process(self, intents, text, conv_id): """Detect a matching intent.""" @@ -203,7 +207,7 @@ class HangoutsBot: """Run once when Home Assistant stops.""" await self.async_disconnect() - async def _async_send_message(self, message, targets): + async def _async_send_message(self, message, targets, data): conversations = [] for target in targets: conversation = None @@ -233,10 +237,48 @@ class HangoutsBot: del segment['parse_str'] messages.append(ChatMessageSegment(**segment)) + image_file = None + if data: + if data.get('image_url'): + uri = data.get('image_url') + try: + websession = async_get_clientsession(self.hass) + async with websession.get(uri, timeout=5) as response: + if response.status != 200: + _LOGGER.error( + 'Fetch image failed, %s, %s', + response.status, + response + ) + image_file = None + else: + image_data = await response.read() + image_file = io.BytesIO(image_data) + image_file.name = "image.png" + except (asyncio.TimeoutError, aiohttp.ClientError) as error: + _LOGGER.error( + 'Failed to fetch image, %s', + type(error) + ) + image_file = None + elif data.get('image_file'): + uri = data.get('image_file') + if self.hass.config.is_allowed_path(uri): + try: + image_file = open(uri, 'rb') + except IOError as error: + _LOGGER.error( + 'Image file I/O error(%s): %s', + error.errno, + error.strerror + ) + else: + _LOGGER.error('Path "%s" not allowed', uri) + if not messages: return False for conv in conversations: - await conv.send_message(messages) + await conv.send_message(messages, image_file) async def _async_list_conversations(self): import hangups @@ -261,7 +303,8 @@ class HangoutsBot: async def async_handle_send_message(self, service): """Handle the send_message service.""" await self._async_send_message(service.data[ATTR_MESSAGE], - service.data[ATTR_TARGET]) + service.data[ATTR_TARGET], + service.data[ATTR_DATA]) async def async_handle_update_users_and_conversations(self, _=None): """Handle the update_users_and_conversations service.""" diff --git a/homeassistant/components/hangouts/services.yaml b/homeassistant/components/hangouts/services.yaml index 5d314bc2479..ded324d2de9 100644 --- a/homeassistant/components/hangouts/services.yaml +++ b/homeassistant/components/hangouts/services.yaml @@ -9,4 +9,8 @@ send_message: example: '[{"id": "UgxrXzVrARmjx_C6AZx4AaABAagBo-6UCw"}, {"name": "Test Conversation"}]' message: description: List of message segments, only the "text" field is required in every segment. [Required] - example: '[{"text":"test", "is_bold": false, "is_italic": false, "is_strikethrough": false, "is_underline": false, "parse_str": false, "link_target": "http://google.com"}, ...]' \ No newline at end of file + example: '[{"text":"test", "is_bold": false, "is_italic": false, "is_strikethrough": false, "is_underline": false, "parse_str": false, "link_target": "http://google.com"}, ...]' + data: + description: Other options ['image_file' / 'image_url'] + example: '{ "image_file": "file" }' or '{ "image_url": "url" }' + From 0dbfd77402777f195fbc87364e185d7914dd3f4b Mon Sep 17 00:00:00 2001 From: emontnemery Date: Tue, 25 Sep 2018 17:15:39 +0200 Subject: [PATCH 042/247] Remove discovered MQTT climate device when discovery topic is cleared (#16856) --- homeassistant/components/climate/mqtt.py | 27 ++++++++++++++++-------- tests/components/climate/test_mqtt.py | 24 ++++++++++++++++++++- 2 files changed, 41 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/climate/mqtt.py b/homeassistant/components/climate/mqtt.py index 9e227e002b5..66f76ac1aaa 100644 --- a/homeassistant/components/climate/mqtt.py +++ b/homeassistant/components/climate/mqtt.py @@ -21,8 +21,9 @@ from homeassistant.components.climate import ( from homeassistant.const import ( STATE_ON, STATE_OFF, ATTR_TEMPERATURE, CONF_NAME, CONF_VALUE_TEMPLATE) from homeassistant.components.mqtt import ( - CONF_AVAILABILITY_TOPIC, CONF_QOS, CONF_RETAIN, CONF_PAYLOAD_AVAILABLE, - CONF_PAYLOAD_NOT_AVAILABLE, MQTT_BASE_PLATFORM_SCHEMA, MqttAvailability) + ATTR_DISCOVERY_HASH, CONF_AVAILABILITY_TOPIC, CONF_QOS, CONF_RETAIN, + CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, + MQTT_BASE_PLATFORM_SCHEMA, MqttAvailability, MqttDiscoveryUpdate) import homeassistant.helpers.config_validation as cv from homeassistant.components.fan import (SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH) @@ -153,6 +154,10 @@ def async_setup_platform(hass, config, async_add_entities, value_templates[key] = config.get(key) value_templates[key].hass = hass + discovery_hash = None + if discovery_info is not None and ATTR_DISCOVERY_HASH in discovery_info: + discovery_hash = discovery_info[ATTR_DISCOVERY_HASH] + async_add_entities([ MqttClimate( hass, @@ -194,11 +199,12 @@ def async_setup_platform(hass, config, async_add_entities, config.get(CONF_PAYLOAD_AVAILABLE), config.get(CONF_PAYLOAD_NOT_AVAILABLE), config.get(CONF_MIN_TEMP), - config.get(CONF_MAX_TEMP)) - ]) + config.get(CONF_MAX_TEMP), + discovery_hash, + )]) -class MqttClimate(MqttAvailability, ClimateDevice): +class MqttClimate(MqttAvailability, MqttDiscoveryUpdate, ClimateDevice): """Representation of an MQTT climate device.""" def __init__(self, hass, name, topic, value_templates, qos, retain, @@ -207,10 +213,11 @@ class MqttClimate(MqttAvailability, ClimateDevice): current_swing_mode, current_operation, aux, send_if_off, payload_on, payload_off, availability_topic, payload_available, payload_not_available, - min_temp, max_temp): + min_temp, max_temp, discovery_hash): """Initialize the climate device.""" - super().__init__(availability_topic, qos, payload_available, - payload_not_available) + MqttAvailability.__init__(self, availability_topic, qos, + payload_available, payload_not_available) + MqttDiscoveryUpdate.__init__(self, discovery_hash) self.hass = hass self._name = name self._topic = topic @@ -235,11 +242,13 @@ class MqttClimate(MqttAvailability, ClimateDevice): self._payload_off = payload_off self._min_temp = min_temp self._max_temp = max_temp + self._discovery_hash = discovery_hash @asyncio.coroutine def async_added_to_hass(self): """Handle being added to home assistant.""" - yield from super().async_added_to_hass() + yield from MqttAvailability.async_added_to_hass(self) + yield from MqttDiscoveryUpdate.async_added_to_hass(self) @callback def handle_current_temp_received(topic, payload, qos): diff --git a/tests/components/climate/test_mqtt.py b/tests/components/climate/test_mqtt.py index f46a23e4f97..6c43c4d9dbe 100644 --- a/tests/components/climate/test_mqtt.py +++ b/tests/components/climate/test_mqtt.py @@ -12,8 +12,10 @@ from homeassistant.components.climate import ( SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, SUPPORT_FAN_MODE, SUPPORT_SWING_MODE, SUPPORT_HOLD_MODE, SUPPORT_AWAY_MODE, SUPPORT_AUX_HEAT, DEFAULT_MIN_TEMP, DEFAULT_MAX_TEMP) +from homeassistant.components.mqtt.discovery import async_start from tests.common import (get_test_home_assistant, mock_mqtt_component, - fire_mqtt_message, mock_component) + async_fire_mqtt_message, fire_mqtt_message, + mock_component) ENTITY_CLIMATE = 'climate.test' @@ -649,3 +651,23 @@ class TestMQTTClimate(unittest.TestCase): self.assertIsInstance(max_temp, float) self.assertEqual(60, max_temp) + + +async def test_discovery_removal_climate(hass, mqtt_mock, caplog): + """Test removal of discovered climate.""" + await async_start(hass, 'homeassistant', {}) + data = ( + '{ "name": "Beer" }' + ) + async_fire_mqtt_message(hass, 'homeassistant/climate/bla/config', + data) + await hass.async_block_till_done() + state = hass.states.get('climate.beer') + assert state is not None + assert state.name == 'Beer' + async_fire_mqtt_message(hass, 'homeassistant/climate/bla/config', + '') + await hass.async_block_till_done() + await hass.async_block_till_done() + state = hass.states.get('climate.beer') + assert state is None From 399040de469450f4d0a07f4ded0c3ea96bbfb51c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 25 Sep 2018 17:19:46 +0200 Subject: [PATCH 043/247] Fix files left behind (#16855) * Light demo test to not write entity registry * Fix Manual MQTT alarm control panel --- .../alarm_control_panel/test_manual_mqtt.py | 3 +- tests/components/light/test_demo.py | 134 +++++++++--------- 2 files changed, 67 insertions(+), 70 deletions(-) diff --git a/tests/components/alarm_control_panel/test_manual_mqtt.py b/tests/components/alarm_control_panel/test_manual_mqtt.py index 5b601f089dd..0158381b526 100644 --- a/tests/components/alarm_control_panel/test_manual_mqtt.py +++ b/tests/components/alarm_control_panel/test_manual_mqtt.py @@ -1,7 +1,7 @@ """The tests for the manual_mqtt Alarm Control Panel component.""" from datetime import timedelta import unittest -from unittest.mock import patch +from unittest.mock import patch, Mock from homeassistant.setup import setup_component from homeassistant.const import ( @@ -23,6 +23,7 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): def setUp(self): # pylint: disable=invalid-name """Set up things to be run when tests are started.""" self.hass = get_test_home_assistant() + self.hass.config_entries._async_schedule_save = Mock() self.mock_publish = mock_mqtt_component(self.hass) def tearDown(self): # pylint: disable=invalid-name diff --git a/tests/components/light/test_demo.py b/tests/components/light/test_demo.py index db575bba5ba..c61bd83cde2 100644 --- a/tests/components/light/test_demo.py +++ b/tests/components/light/test_demo.py @@ -1,81 +1,77 @@ """The tests for the demo light component.""" -# pylint: disable=protected-access -import unittest +import pytest -from homeassistant.setup import setup_component -import homeassistant.components.light as light - -from tests.common import get_test_home_assistant +from homeassistant.setup import async_setup_component +from homeassistant.components import light ENTITY_LIGHT = 'light.bed_light' -class TestDemoLight(unittest.TestCase): - """Test the demo light.""" - - # pylint: disable=invalid-name - def setUp(self): - """Set up things to be run when tests are started.""" - self.hass = get_test_home_assistant() - self.assertTrue(setup_component(self.hass, light.DOMAIN, {'light': { +@pytest.fixture(autouse=True) +def setup_comp(hass): + """Set up demo component.""" + hass.loop.run_until_complete(async_setup_component(hass, light.DOMAIN, { + 'light': { 'platform': 'demo', }})) - # pylint: disable=invalid-name - def tearDown(self): - """Stop down everything that was started.""" - self.hass.stop() - def test_state_attributes(self): - """Test light state attributes.""" - light.turn_on( - self.hass, ENTITY_LIGHT, xy_color=(.4, .4), brightness=25) - self.hass.block_till_done() - state = self.hass.states.get(ENTITY_LIGHT) - self.assertTrue(light.is_on(self.hass, ENTITY_LIGHT)) - self.assertEqual((0.4, 0.4), state.attributes.get( - light.ATTR_XY_COLOR)) - self.assertEqual(25, state.attributes.get(light.ATTR_BRIGHTNESS)) - self.assertEqual( - (255, 234, 164), state.attributes.get(light.ATTR_RGB_COLOR)) - self.assertEqual('rainbow', state.attributes.get(light.ATTR_EFFECT)) - light.turn_on( - self.hass, ENTITY_LIGHT, rgb_color=(251, 253, 255), - white_value=254) - self.hass.block_till_done() - state = self.hass.states.get(ENTITY_LIGHT) - self.assertEqual(254, state.attributes.get(light.ATTR_WHITE_VALUE)) - self.assertEqual( - (250, 252, 255), state.attributes.get(light.ATTR_RGB_COLOR)) - self.assertEqual( - (0.319, 0.326), state.attributes.get(light.ATTR_XY_COLOR)) - light.turn_on(self.hass, ENTITY_LIGHT, color_temp=400, effect='none') - self.hass.block_till_done() - state = self.hass.states.get(ENTITY_LIGHT) - self.assertEqual(400, state.attributes.get(light.ATTR_COLOR_TEMP)) - self.assertEqual(153, state.attributes.get(light.ATTR_MIN_MIREDS)) - self.assertEqual(500, state.attributes.get(light.ATTR_MAX_MIREDS)) - self.assertEqual('none', state.attributes.get(light.ATTR_EFFECT)) - light.turn_on(self.hass, ENTITY_LIGHT, kelvin=3000, brightness_pct=50) - self.hass.block_till_done() - state = self.hass.states.get(ENTITY_LIGHT) - self.assertEqual(333, state.attributes.get(light.ATTR_COLOR_TEMP)) - self.assertEqual(127, state.attributes.get(light.ATTR_BRIGHTNESS)) +async def test_state_attributes(hass): + """Test light state attributes.""" + light.async_turn_on( + hass, ENTITY_LIGHT, xy_color=(.4, .4), brightness=25) + await hass.async_block_till_done() + state = hass.states.get(ENTITY_LIGHT) + assert light.is_on(hass, ENTITY_LIGHT) + assert (0.4, 0.4) == state.attributes.get(light.ATTR_XY_COLOR) + assert 25 == state.attributes.get(light.ATTR_BRIGHTNESS) + assert (255, 234, 164) == state.attributes.get(light.ATTR_RGB_COLOR) + assert 'rainbow' == state.attributes.get(light.ATTR_EFFECT) + light.async_turn_on( + hass, ENTITY_LIGHT, rgb_color=(251, 253, 255), + white_value=254) + await hass.async_block_till_done() + state = hass.states.get(ENTITY_LIGHT) + assert 254 == state.attributes.get(light.ATTR_WHITE_VALUE) + assert (250, 252, 255) == state.attributes.get(light.ATTR_RGB_COLOR) + assert (0.319, 0.326) == state.attributes.get(light.ATTR_XY_COLOR) + light.async_turn_on(hass, ENTITY_LIGHT, color_temp=400, effect='none') + await hass.async_block_till_done() + state = hass.states.get(ENTITY_LIGHT) + assert 400 == state.attributes.get(light.ATTR_COLOR_TEMP) + assert 153 == state.attributes.get(light.ATTR_MIN_MIREDS) + assert 500 == state.attributes.get(light.ATTR_MAX_MIREDS) + assert 'none' == state.attributes.get(light.ATTR_EFFECT) + light.async_turn_on(hass, ENTITY_LIGHT, kelvin=3000, brightness_pct=50) + await hass.async_block_till_done() + state = hass.states.get(ENTITY_LIGHT) + assert 333 == state.attributes.get(light.ATTR_COLOR_TEMP) + assert 127 == state.attributes.get(light.ATTR_BRIGHTNESS) - def test_turn_off(self): - """Test light turn off method.""" - light.turn_on(self.hass, ENTITY_LIGHT) - self.hass.block_till_done() - self.assertTrue(light.is_on(self.hass, ENTITY_LIGHT)) - light.turn_off(self.hass, ENTITY_LIGHT) - self.hass.block_till_done() - self.assertFalse(light.is_on(self.hass, ENTITY_LIGHT)) - def test_turn_off_without_entity_id(self): - """Test light turn off all lights.""" - light.turn_on(self.hass, ENTITY_LIGHT) - self.hass.block_till_done() - self.assertTrue(light.is_on(self.hass, ENTITY_LIGHT)) - light.turn_off(self.hass) - self.hass.block_till_done() - self.assertFalse(light.is_on(self.hass, ENTITY_LIGHT)) +async def test_turn_off(hass): + """Test light turn off method.""" + await hass.services.async_call('light', 'turn_on', { + 'entity_id': ENTITY_LIGHT + }, blocking=True) + + assert light.is_on(hass, ENTITY_LIGHT) + + await hass.services.async_call('light', 'turn_off', { + 'entity_id': ENTITY_LIGHT + }, blocking=True) + + assert not light.is_on(hass, ENTITY_LIGHT) + + +async def test_turn_off_without_entity_id(hass): + """Test light turn off all lights.""" + await hass.services.async_call('light', 'turn_on', { + }, blocking=True) + + assert light.is_on(hass, ENTITY_LIGHT) + + await hass.services.async_call('light', 'turn_off', { + }, blocking=True) + + assert not light.is_on(hass, ENTITY_LIGHT) From eb59f2dd3cdb94d9bc3481827132ae6787d4b6da Mon Sep 17 00:00:00 2001 From: emontnemery Date: Tue, 25 Sep 2018 19:32:04 +0200 Subject: [PATCH 044/247] Move MQTT discovery removal tests to platform test files (#16861) --- .../alarm_control_panel/test_mqtt.py | 34 +++++- tests/components/binary_sensor/test_mqtt.py | 22 ++++ tests/components/light/test_mqtt.py | 30 ++++- tests/components/mqtt/test_discovery.py | 108 ------------------ tests/components/switch/test_mqtt.py | 28 +++++ 5 files changed, 111 insertions(+), 111 deletions(-) diff --git a/tests/components/alarm_control_panel/test_mqtt.py b/tests/components/alarm_control_panel/test_mqtt.py index ce152a3d7c9..3a68b3cee44 100644 --- a/tests/components/alarm_control_panel/test_mqtt.py +++ b/tests/components/alarm_control_panel/test_mqtt.py @@ -7,10 +7,11 @@ from homeassistant.const import ( STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED, STATE_UNAVAILABLE, STATE_UNKNOWN) from homeassistant.components import alarm_control_panel +from homeassistant.components.mqtt.discovery import async_start from tests.common import ( - mock_mqtt_component, fire_mqtt_message, get_test_home_assistant, - assert_setup_component) + mock_mqtt_component, async_fire_mqtt_message, fire_mqtt_message, + get_test_home_assistant, assert_setup_component) CODE = 'HELLO_CODE' @@ -239,3 +240,32 @@ class TestAlarmControlPanelMQTT(unittest.TestCase): self.assertEqual(STATE_UNAVAILABLE, state.state) fire_mqtt_message(self.hass, 'availability-topic', 'good') + + +async def test_discovery_removal_alarm(hass, mqtt_mock, caplog): + """Test removal of discovered alarm_control_panel.""" + await async_start(hass, 'homeassistant', {}) + + data = ( + '{ "name": "Beer",' + ' "status_topic": "test_topic",' + ' "command_topic": "test_topic" }' + ) + + async_fire_mqtt_message(hass, + 'homeassistant/alarm_control_panel/bla/config', + data) + await hass.async_block_till_done() + + state = hass.states.get('alarm_control_panel.beer') + assert state is not None + assert state.name == 'Beer' + + async_fire_mqtt_message(hass, + 'homeassistant/alarm_control_panel/bla/config', + '') + await hass.async_block_till_done() + await hass.async_block_till_done() + + state = hass.states.get('alarm_control_panel.beer') + assert state is None diff --git a/tests/components/binary_sensor/test_mqtt.py b/tests/components/binary_sensor/test_mqtt.py index 0bc8c6c81fb..dfa01898ba4 100644 --- a/tests/components/binary_sensor/test_mqtt.py +++ b/tests/components/binary_sensor/test_mqtt.py @@ -4,6 +4,7 @@ import unittest import homeassistant.core as ha from homeassistant.setup import setup_component, async_setup_component import homeassistant.components.binary_sensor as binary_sensor +from homeassistant.components.mqtt.discovery import async_start from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.const import EVENT_STATE_CHANGED, STATE_UNAVAILABLE @@ -226,3 +227,24 @@ async def test_unique_id(hass): async_fire_mqtt_message(hass, 'test-topic', 'payload') await hass.async_block_till_done() assert len(hass.states.async_all()) == 1 + + +async def test_discovery_removal_binary_sensor(hass, mqtt_mock, caplog): + """Test removal of discovered binary_sensor.""" + await async_start(hass, 'homeassistant', {}) + data = ( + '{ "name": "Beer",' + ' "status_topic": "test_topic" }' + ) + async_fire_mqtt_message(hass, 'homeassistant/binary_sensor/bla/config', + data) + await hass.async_block_till_done() + state = hass.states.get('binary_sensor.beer') + assert state is not None + assert state.name == 'Beer' + async_fire_mqtt_message(hass, 'homeassistant/binary_sensor/bla/config', + '') + await hass.async_block_till_done() + await hass.async_block_till_done() + state = hass.states.get('binary_sensor.beer') + assert state is None diff --git a/tests/components/light/test_mqtt.py b/tests/components/light/test_mqtt.py index 1245411dcc4..3640a6a0130 100644 --- a/tests/components/light/test_mqtt.py +++ b/tests/components/light/test_mqtt.py @@ -146,10 +146,11 @@ from homeassistant.setup import setup_component from homeassistant.const import ( STATE_ON, STATE_OFF, STATE_UNAVAILABLE, ATTR_ASSUMED_STATE) import homeassistant.components.light as light +from homeassistant.components.mqtt.discovery import async_start import homeassistant.core as ha from tests.common import ( assert_setup_component, get_test_home_assistant, mock_mqtt_component, - fire_mqtt_message, mock_coro) + async_fire_mqtt_message, fire_mqtt_message, mock_coro) class TestLightMQTT(unittest.TestCase): @@ -876,3 +877,30 @@ class TestLightMQTT(unittest.TestCase): state = self.hass.states.get('light.test') self.assertEqual(STATE_UNAVAILABLE, state.state) + + +async def test_discovery_removal_light(hass, mqtt_mock, caplog): + """Test removal of discovered light.""" + await async_start(hass, 'homeassistant', {}) + + data = ( + '{ "name": "Beer",' + ' "status_topic": "test_topic",' + ' "command_topic": "test_topic" }' + ) + + async_fire_mqtt_message(hass, 'homeassistant/light/bla/config', + data) + await hass.async_block_till_done() + + state = hass.states.get('light.beer') + assert state is not None + assert state.name == 'Beer' + + async_fire_mqtt_message(hass, 'homeassistant/light/bla/config', + '') + await hass.async_block_till_done() + await hass.async_block_till_done() + + state = hass.states.get('light.beer') + assert state is None diff --git a/tests/components/mqtt/test_discovery.py b/tests/components/mqtt/test_discovery.py index 0656bbdc300..9e0ef14a3fa 100644 --- a/tests/components/mqtt/test_discovery.py +++ b/tests/components/mqtt/test_discovery.py @@ -181,111 +181,3 @@ def test_non_duplicate_discovery(hass, mqtt_mock, caplog): assert state_duplicate is None assert 'Component has already been discovered: ' \ 'binary_sensor bla' in caplog.text - - -@asyncio.coroutine -def test_discovery_removal(hass, mqtt_mock, caplog): - """Test expansion of abbreviated discovery payload.""" - yield from async_start(hass, 'homeassistant', {}) - - data = ( - '{ "name": "Beer",' - ' "status_topic": "test_topic",' - ' "command_topic": "test_topic" }' - ) - - async_fire_mqtt_message(hass, 'homeassistant/switch/bla/config', - data) - yield from hass.async_block_till_done() - - state = hass.states.get('switch.beer') - assert state is not None - assert state.name == 'Beer' - - async_fire_mqtt_message(hass, 'homeassistant/switch/bla/config', - '') - yield from hass.async_block_till_done() - yield from hass.async_block_till_done() - - state = hass.states.get('switch.beer') - assert state is None - - -@asyncio.coroutine -def test_discovery_removal_binary_sensor(hass, mqtt_mock, caplog): - """Test removal of discovered binary_sensor.""" - yield from async_start(hass, 'homeassistant', {}) - data = ( - '{ "name": "Beer",' - ' "status_topic": "test_topic" }' - ) - async_fire_mqtt_message(hass, 'homeassistant/binary_sensor/bla/config', - data) - yield from hass.async_block_till_done() - state = hass.states.get('binary_sensor.beer') - assert state is not None - assert state.name == 'Beer' - async_fire_mqtt_message(hass, 'homeassistant/binary_sensor/bla/config', - '') - yield from hass.async_block_till_done() - yield from hass.async_block_till_done() - state = hass.states.get('binary_sensor.beer') - assert state is None - - -@asyncio.coroutine -def test_discovery_removal_light(hass, mqtt_mock, caplog): - """Test removal of discovered light.""" - yield from async_start(hass, 'homeassistant', {}) - - data = ( - '{ "name": "Beer",' - ' "status_topic": "test_topic",' - ' "command_topic": "test_topic" }' - ) - - async_fire_mqtt_message(hass, 'homeassistant/light/bla/config', - data) - yield from hass.async_block_till_done() - - state = hass.states.get('light.beer') - assert state is not None - assert state.name == 'Beer' - - async_fire_mqtt_message(hass, 'homeassistant/light/bla/config', - '') - yield from hass.async_block_till_done() - yield from hass.async_block_till_done() - - state = hass.states.get('light.beer') - assert state is None - - -@asyncio.coroutine -def test_discovery_removal_alarm(hass, mqtt_mock, caplog): - """Test removal of discovered alarm_control_panel.""" - yield from async_start(hass, 'homeassistant', {}) - - data = ( - '{ "name": "Beer",' - ' "status_topic": "test_topic",' - ' "command_topic": "test_topic" }' - ) - - async_fire_mqtt_message(hass, - 'homeassistant/alarm_control_panel/bla/config', - data) - yield from hass.async_block_till_done() - - state = hass.states.get('alarm_control_panel.beer') - assert state is not None - assert state.name == 'Beer' - - async_fire_mqtt_message(hass, - 'homeassistant/alarm_control_panel/bla/config', - '') - yield from hass.async_block_till_done() - yield from hass.async_block_till_done() - - state = hass.states.get('alarm_control_panel.beer') - assert state is None diff --git a/tests/components/switch/test_mqtt.py b/tests/components/switch/test_mqtt.py index 321db7ed118..cad93e3bfce 100644 --- a/tests/components/switch/test_mqtt.py +++ b/tests/components/switch/test_mqtt.py @@ -7,6 +7,7 @@ from homeassistant.const import STATE_ON, STATE_OFF, STATE_UNAVAILABLE,\ ATTR_ASSUMED_STATE import homeassistant.core as ha import homeassistant.components.switch as switch +from homeassistant.components.mqtt.discovery import async_start from tests.common import ( mock_mqtt_component, fire_mqtt_message, get_test_home_assistant, mock_coro, async_mock_mqtt_component, async_fire_mqtt_message) @@ -306,3 +307,30 @@ async def test_unique_id(hass): assert len(hass.states.async_entity_ids()) == 2 # all switches group is 1, unique id created is 1 + + +async def test_discovery_removal_switch(hass, mqtt_mock, caplog): + """Test expansion of discovered switch.""" + await async_start(hass, 'homeassistant', {}) + + data = ( + '{ "name": "Beer",' + ' "status_topic": "test_topic",' + ' "command_topic": "test_topic" }' + ) + + async_fire_mqtt_message(hass, 'homeassistant/switch/bla/config', + data) + await hass.async_block_till_done() + + state = hass.states.get('switch.beer') + assert state is not None + assert state.name == 'Beer' + + async_fire_mqtt_message(hass, 'homeassistant/switch/bla/config', + '') + await hass.async_block_till_done() + await hass.async_block_till_done() + + state = hass.states.get('switch.beer') + assert state is None From 422ccc1a284f1076434e3a858840b7e8d387874d Mon Sep 17 00:00:00 2001 From: emontnemery Date: Tue, 25 Sep 2018 19:32:16 +0200 Subject: [PATCH 045/247] Remove discovered MQTT sensor device when discovery topic is cleared (#16860) --- homeassistant/components/sensor/mqtt.py | 25 +++++++++++++++++-------- tests/components/sensor/test_mqtt.py | 22 ++++++++++++++++++++++ 2 files changed, 39 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/sensor/mqtt.py b/homeassistant/components/sensor/mqtt.py index 6cf2d55755d..56e4055ea87 100644 --- a/homeassistant/components/sensor/mqtt.py +++ b/homeassistant/components/sensor/mqtt.py @@ -13,8 +13,9 @@ import voluptuous as vol from homeassistant.core import callback from homeassistant.components.mqtt import ( - CONF_AVAILABILITY_TOPIC, CONF_STATE_TOPIC, CONF_PAYLOAD_AVAILABLE, - CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, MqttAvailability) + ATTR_DISCOVERY_HASH, CONF_AVAILABILITY_TOPIC, CONF_STATE_TOPIC, + CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, + MqttAvailability, MqttDiscoveryUpdate) from homeassistant.components.sensor import DEVICE_CLASSES_SCHEMA from homeassistant.const import ( CONF_FORCE_UPDATE, CONF_NAME, CONF_VALUE_TEMPLATE, STATE_UNKNOWN, @@ -60,6 +61,10 @@ async def async_setup_platform(hass: HomeAssistantType, config: ConfigType, if value_template is not None: value_template.hass = hass + discovery_hash = None + if discovery_info is not None and ATTR_DISCOVERY_HASH in discovery_info: + discovery_hash = discovery_info[ATTR_DISCOVERY_HASH] + async_add_entities([MqttSensor( config.get(CONF_NAME), config.get(CONF_STATE_TOPIC), @@ -75,20 +80,22 @@ async def async_setup_platform(hass: HomeAssistantType, config: ConfigType, config.get(CONF_AVAILABILITY_TOPIC), config.get(CONF_PAYLOAD_AVAILABLE), config.get(CONF_PAYLOAD_NOT_AVAILABLE), + discovery_hash, )]) -class MqttSensor(MqttAvailability, Entity): +class MqttSensor(MqttAvailability, MqttDiscoveryUpdate, Entity): """Representation of a sensor that can be updated using MQTT.""" def __init__(self, name, state_topic, qos, unit_of_measurement, force_update, expire_after, icon, device_class: Optional[str], value_template, json_attributes, unique_id: Optional[str], - availability_topic, payload_available, - payload_not_available): + availability_topic, payload_available, payload_not_available, + discovery_hash): """Initialize the sensor.""" - super().__init__(availability_topic, qos, payload_available, - payload_not_available) + MqttAvailability.__init__(self, availability_topic, qos, + payload_available, payload_not_available) + MqttDiscoveryUpdate.__init__(self, discovery_hash) self._state = STATE_UNKNOWN self._name = name self._state_topic = state_topic @@ -103,10 +110,12 @@ class MqttSensor(MqttAvailability, Entity): self._json_attributes = set(json_attributes) self._unique_id = unique_id self._attributes = None + self._discovery_hash = discovery_hash async def async_added_to_hass(self): """Subscribe to MQTT events.""" - await super().async_added_to_hass() + await MqttAvailability.async_added_to_hass(self) + await MqttDiscoveryUpdate.async_added_to_hass(self) @callback def message_received(topic, payload, qos): diff --git a/tests/components/sensor/test_mqtt.py b/tests/components/sensor/test_mqtt.py index feef647b7b7..527c92997ec 100644 --- a/tests/components/sensor/test_mqtt.py +++ b/tests/components/sensor/test_mqtt.py @@ -6,6 +6,7 @@ from unittest.mock import patch import homeassistant.core as ha from homeassistant.setup import setup_component, async_setup_component +from homeassistant.components.mqtt.discovery import async_start import homeassistant.components.sensor as sensor from homeassistant.const import EVENT_STATE_CHANGED, STATE_UNAVAILABLE import homeassistant.util.dt as dt_util @@ -387,3 +388,24 @@ async def test_unique_id(hass): await hass.async_block_till_done() assert len(hass.states.async_all()) == 1 + + +async def test_discovery_removal_sensor(hass, mqtt_mock, caplog): + """Test removal of discovered sensor.""" + await async_start(hass, 'homeassistant', {}) + data = ( + '{ "name": "Beer",' + ' "status_topic": "test_topic" }' + ) + async_fire_mqtt_message(hass, 'homeassistant/sensor/bla/config', + data) + await hass.async_block_till_done() + state = hass.states.get('sensor.beer') + assert state is not None + assert state.name == 'Beer' + async_fire_mqtt_message(hass, 'homeassistant/sensor/bla/config', + '') + await hass.async_block_till_done() + await hass.async_block_till_done() + state = hass.states.get('sensor.beer') + assert state is None From c3f58b8c746ff0acf3e5ec5e395a99af4e6ce604 Mon Sep 17 00:00:00 2001 From: emontnemery Date: Tue, 25 Sep 2018 19:32:25 +0200 Subject: [PATCH 046/247] Remove discovered MQTT lock device when discovery topic is cleared (#16859) --- homeassistant/components/lock/mqtt.py | 27 ++++++++++++++++++--------- tests/components/lock/test_mqtt.py | 25 ++++++++++++++++++++++++- 2 files changed, 42 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/lock/mqtt.py b/homeassistant/components/lock/mqtt.py index 103864a6bfd..b9b094b615b 100644 --- a/homeassistant/components/lock/mqtt.py +++ b/homeassistant/components/lock/mqtt.py @@ -12,9 +12,9 @@ import voluptuous as vol from homeassistant.core import callback from homeassistant.components.lock import LockDevice from homeassistant.components.mqtt import ( - CONF_AVAILABILITY_TOPIC, CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, - CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, CONF_RETAIN, - MqttAvailability) + 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) from homeassistant.const import ( CONF_NAME, CONF_OPTIMISTIC, CONF_VALUE_TEMPLATE) from homeassistant.components import mqtt @@ -52,6 +52,10 @@ def async_setup_platform(hass, config, async_add_entities, if value_template is not None: value_template.hass = hass + discovery_hash = None + if discovery_info is not None and ATTR_DISCOVERY_HASH in discovery_info: + discovery_hash = discovery_info[ATTR_DISCOVERY_HASH] + async_add_entities([MqttLock( config.get(CONF_NAME), config.get(CONF_STATE_TOPIC), @@ -64,19 +68,22 @@ def async_setup_platform(hass, config, async_add_entities, value_template, config.get(CONF_AVAILABILITY_TOPIC), config.get(CONF_PAYLOAD_AVAILABLE), - config.get(CONF_PAYLOAD_NOT_AVAILABLE) + config.get(CONF_PAYLOAD_NOT_AVAILABLE), + discovery_hash, )]) -class MqttLock(MqttAvailability, LockDevice): +class MqttLock(MqttAvailability, MqttDiscoveryUpdate, 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): + availability_topic, payload_available, payload_not_available, + discovery_hash): """Initialize the lock.""" - super().__init__(availability_topic, qos, payload_available, - payload_not_available) + MqttAvailability.__init__(self, availability_topic, qos, + payload_available, payload_not_available) + MqttDiscoveryUpdate.__init__(self, discovery_hash) self._state = False self._name = name self._state_topic = state_topic @@ -87,11 +94,13 @@ class MqttLock(MqttAvailability, LockDevice): self._payload_unlock = payload_unlock self._optimistic = optimistic self._template = value_template + self._discovery_hash = discovery_hash @asyncio.coroutine def async_added_to_hass(self): """Subscribe to MQTT events.""" - yield from super().async_added_to_hass() + yield from MqttAvailability.async_added_to_hass(self) + yield from MqttDiscoveryUpdate.async_added_to_hass(self) @callback def message_received(topic, payload, qos): diff --git a/tests/components/lock/test_mqtt.py b/tests/components/lock/test_mqtt.py index 4ef8532f39e..f85c6a11774 100644 --- a/tests/components/lock/test_mqtt.py +++ b/tests/components/lock/test_mqtt.py @@ -5,8 +5,10 @@ from homeassistant.setup import setup_component from homeassistant.const import ( STATE_LOCKED, STATE_UNLOCKED, STATE_UNAVAILABLE, ATTR_ASSUMED_STATE) import homeassistant.components.lock as lock +from homeassistant.components.mqtt.discovery import async_start from tests.common import ( - mock_mqtt_component, fire_mqtt_message, get_test_home_assistant) + mock_mqtt_component, async_fire_mqtt_message, fire_mqtt_message, + get_test_home_assistant) class TestLockMQTT(unittest.TestCase): @@ -172,3 +174,24 @@ class TestLockMQTT(unittest.TestCase): state = self.hass.states.get('lock.test') self.assertEqual(STATE_UNAVAILABLE, state.state) + + +async def test_discovery_removal_lock(hass, mqtt_mock, caplog): + """Test removal of discovered lock.""" + await async_start(hass, 'homeassistant', {}) + data = ( + '{ "name": "Beer",' + ' "command_topic": "test_topic" }' + ) + async_fire_mqtt_message(hass, 'homeassistant/lock/bla/config', + data) + 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', + '') + await hass.async_block_till_done() + await hass.async_block_till_done() + state = hass.states.get('lock.beer') + assert state is None From 4a265f37e01682730905409754336adb9fc59b2e Mon Sep 17 00:00:00 2001 From: emontnemery Date: Tue, 25 Sep 2018 19:32:31 +0200 Subject: [PATCH 047/247] Remove discovered MQTT fan device when discovery topic is cleared (#16858) --- homeassistant/components/fan/mqtt.py | 24 ++++++++++++++++-------- tests/components/fan/test_mqtt.py | 25 ++++++++++++++++++++++++- 2 files changed, 40 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/fan/mqtt.py b/homeassistant/components/fan/mqtt.py index db3cfab3608..4ff8e1ec757 100644 --- a/homeassistant/components/fan/mqtt.py +++ b/homeassistant/components/fan/mqtt.py @@ -14,9 +14,9 @@ from homeassistant.const import ( CONF_NAME, CONF_OPTIMISTIC, CONF_STATE, STATE_ON, STATE_OFF, CONF_PAYLOAD_OFF, CONF_PAYLOAD_ON) from homeassistant.components.mqtt import ( - CONF_AVAILABILITY_TOPIC, CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, - CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, CONF_RETAIN, - MqttAvailability) + 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) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import HomeAssistantType, ConfigType from homeassistant.components.fan import (SPEED_LOW, SPEED_MEDIUM, @@ -83,6 +83,10 @@ async def async_setup_platform(hass: HomeAssistantType, config: ConfigType, if discovery_info is not None: config = PLATFORM_SCHEMA(discovery_info) + discovery_hash = None + if discovery_info is not None and ATTR_DISCOVERY_HASH in discovery_info: + discovery_hash = discovery_info[ATTR_DISCOVERY_HASH] + async_add_entities([MqttFan( config.get(CONF_NAME), { @@ -116,18 +120,20 @@ async def async_setup_platform(hass: HomeAssistantType, config: ConfigType, config.get(CONF_AVAILABILITY_TOPIC), config.get(CONF_PAYLOAD_AVAILABLE), config.get(CONF_PAYLOAD_NOT_AVAILABLE), + discovery_hash, )]) -class MqttFan(MqttAvailability, FanEntity): +class MqttFan(MqttAvailability, MqttDiscoveryUpdate, FanEntity): """A MQTT fan component.""" def __init__(self, name, topic, templates, qos, retain, payload, speed_list, optimistic, availability_topic, payload_available, - payload_not_available): + payload_not_available, discovery_hash): """Initialize the MQTT fan.""" - super().__init__(availability_topic, qos, payload_available, - payload_not_available) + MqttAvailability.__init__(self, availability_topic, qos, + payload_available, payload_not_available) + MqttDiscoveryUpdate.__init__(self, discovery_hash) self._name = name self._topic = topic self._qos = qos @@ -148,10 +154,12 @@ class MqttFan(MqttAvailability, FanEntity): is not None and SUPPORT_OSCILLATE) self._supported_features |= (topic[CONF_SPEED_STATE_TOPIC] is not None and SUPPORT_SET_SPEED) + self._discovery_hash = discovery_hash async def async_added_to_hass(self): """Subscribe to MQTT events.""" - await super().async_added_to_hass() + await MqttAvailability.async_added_to_hass(self) + await MqttDiscoveryUpdate.async_added_to_hass(self) templates = {} for key, tpl in list(self._templates.items()): diff --git a/tests/components/fan/test_mqtt.py b/tests/components/fan/test_mqtt.py index 7f69e56218b..5f59ebe4a76 100644 --- a/tests/components/fan/test_mqtt.py +++ b/tests/components/fan/test_mqtt.py @@ -3,10 +3,12 @@ import unittest from homeassistant.setup import setup_component from homeassistant.components import fan +from homeassistant.components.mqtt.discovery import async_start from homeassistant.const import ATTR_ASSUMED_STATE, STATE_UNAVAILABLE from tests.common import ( - mock_mqtt_component, fire_mqtt_message, get_test_home_assistant) + mock_mqtt_component, async_fire_mqtt_message, fire_mqtt_message, + get_test_home_assistant) class TestMqttFan(unittest.TestCase): @@ -102,3 +104,24 @@ class TestMqttFan(unittest.TestCase): state = self.hass.states.get('fan.test') self.assertNotEqual(STATE_UNAVAILABLE, state.state) + + +async def test_discovery_removal_fan(hass, mqtt_mock, caplog): + """Test removal of discovered fan.""" + await async_start(hass, 'homeassistant', {}) + data = ( + '{ "name": "Beer",' + ' "command_topic": "test_topic" }' + ) + async_fire_mqtt_message(hass, 'homeassistant/fan/bla/config', + data) + await hass.async_block_till_done() + state = hass.states.get('fan.beer') + assert state is not None + assert state.name == 'Beer' + async_fire_mqtt_message(hass, 'homeassistant/fan/bla/config', + '') + await hass.async_block_till_done() + await hass.async_block_till_done() + state = hass.states.get('fan.beer') + assert state is None From 4501bdb4a0dd7dccb1bde69e15c1878d2c4480a8 Mon Sep 17 00:00:00 2001 From: emontnemery Date: Tue, 25 Sep 2018 19:32:42 +0200 Subject: [PATCH 048/247] Remove discovered MQTT cover device when discovery topic is cleared (#16857) --- homeassistant/components/cover/mqtt.py | 26 +++++++++++----- tests/components/cover/test_mqtt.py | 41 ++++++++++++++++++++------ 2 files changed, 50 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/cover/mqtt.py b/homeassistant/components/cover/mqtt.py index 977353cb318..ae1162f7120 100644 --- a/homeassistant/components/cover/mqtt.py +++ b/homeassistant/components/cover/mqtt.py @@ -20,9 +20,10 @@ from homeassistant.const import ( CONF_NAME, CONF_VALUE_TEMPLATE, CONF_OPTIMISTIC, STATE_OPEN, STATE_CLOSED, STATE_UNKNOWN) 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, - valid_publish_topic, valid_subscribe_topic, MqttAvailability) + 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) import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -105,6 +106,10 @@ async def async_setup_platform(hass, config, async_add_entities, if set_position_template is not None: set_position_template.hass = hass + discovery_hash = None + if discovery_info is not None and ATTR_DISCOVERY_HASH in discovery_info: + discovery_hash = discovery_info[ATTR_DISCOVERY_HASH] + async_add_entities([MqttCover( config.get(CONF_NAME), config.get(CONF_STATE_TOPIC), @@ -131,10 +136,11 @@ async def async_setup_platform(hass, config, async_add_entities, config.get(CONF_TILT_INVERT_STATE), config.get(CONF_POSITION_TOPIC), set_position_template, + discovery_hash, )]) -class MqttCover(MqttAvailability, CoverDevice): +class MqttCover(MqttAvailability, MqttDiscoveryUpdate, CoverDevice): """Representation of a cover that can be controlled using MQTT.""" def __init__(self, name, state_topic, command_topic, availability_topic, @@ -143,10 +149,12 @@ class MqttCover(MqttAvailability, CoverDevice): payload_stop, payload_available, payload_not_available, optimistic, value_template, tilt_open_position, tilt_closed_position, tilt_min, tilt_max, tilt_optimistic, - tilt_invert, position_topic, set_position_template): + tilt_invert, position_topic, set_position_template, + discovery_hash): """Initialize the cover.""" - super().__init__(availability_topic, qos, payload_available, - payload_not_available) + MqttAvailability.__init__(self, availability_topic, qos, + payload_available, payload_not_available) + MqttDiscoveryUpdate.__init__(self, discovery_hash) self._position = None self._state = None self._name = name @@ -172,10 +180,12 @@ class MqttCover(MqttAvailability, CoverDevice): self._tilt_invert = tilt_invert self._position_topic = position_topic self._set_position_template = set_position_template + self._discovery_hash = discovery_hash async def async_added_to_hass(self): """Subscribe MQTT events.""" - await super().async_added_to_hass() + await MqttAvailability.async_added_to_hass(self) + await MqttDiscoveryUpdate.async_added_to_hass(self) @callback def tilt_updated(topic, payload, qos): diff --git a/tests/components/cover/test_mqtt.py b/tests/components/cover/test_mqtt.py index ad68c2416ca..e75178bf3d7 100644 --- a/tests/components/cover/test_mqtt.py +++ b/tests/components/cover/test_mqtt.py @@ -6,9 +6,11 @@ from homeassistant.const import STATE_OPEN, STATE_CLOSED, STATE_UNKNOWN, \ STATE_UNAVAILABLE, ATTR_ASSUMED_STATE import homeassistant.components.cover as cover from homeassistant.components.cover.mqtt import MqttCover +from homeassistant.components.mqtt.discovery import async_start from tests.common import ( - get_test_home_assistant, mock_mqtt_component, fire_mqtt_message) + get_test_home_assistant, mock_mqtt_component, async_fire_mqtt_message, + fire_mqtt_message) class TestCoverMQTT(unittest.TestCase): @@ -579,7 +581,7 @@ class TestCoverMQTT(unittest.TestCase): 'cover.test', 'state-topic', 'command-topic', None, 'tilt-command-topic', 'tilt-status-topic', 0, False, 'OPEN', 'CLOSE', 'OPEN', 'CLOSE', 'STOP', None, None, - False, None, 100, 0, 0, 100, False, False, None, None) + False, None, 100, 0, 0, 100, False, False, None, None, None) self.assertEqual(44, mqtt_cover.find_percentage_in_range(44)) @@ -589,7 +591,7 @@ class TestCoverMQTT(unittest.TestCase): 'cover.test', 'state-topic', 'command-topic', None, 'tilt-command-topic', 'tilt-status-topic', 0, False, 'OPEN', 'CLOSE', 'OPEN', 'CLOSE', 'STOP', None, None, - False, None, 180, 80, 80, 180, False, False, None, None) + False, None, 180, 80, 80, 180, False, False, None, None, None) self.assertEqual(40, mqtt_cover.find_percentage_in_range(120)) @@ -599,7 +601,7 @@ class TestCoverMQTT(unittest.TestCase): 'cover.test', 'state-topic', 'command-topic', None, 'tilt-command-topic', 'tilt-status-topic', 0, False, 'OPEN', 'CLOSE', 'OPEN', 'CLOSE', 'STOP', None, None, - False, None, 100, 0, 0, 100, False, True, None, None) + False, None, 100, 0, 0, 100, False, True, None, None, None) self.assertEqual(56, mqtt_cover.find_percentage_in_range(44)) @@ -609,7 +611,7 @@ class TestCoverMQTT(unittest.TestCase): 'cover.test', 'state-topic', 'command-topic', None, 'tilt-command-topic', 'tilt-status-topic', 0, False, 'OPEN', 'CLOSE', 'OPEN', 'CLOSE', 'STOP', None, None, - False, None, 180, 80, 80, 180, False, True, None, None) + False, None, 180, 80, 80, 180, False, True, None, None, None) self.assertEqual(60, mqtt_cover.find_percentage_in_range(120)) @@ -619,7 +621,7 @@ class TestCoverMQTT(unittest.TestCase): 'cover.test', 'state-topic', 'command-topic', None, 'tilt-command-topic', 'tilt-status-topic', 0, False, 'OPEN', 'CLOSE', 'OPEN', 'CLOSE', 'STOP', None, None, - False, None, 100, 0, 0, 100, False, False, None, None) + False, None, 100, 0, 0, 100, False, False, None, None, None) self.assertEqual(44, mqtt_cover.find_in_range_from_percent(44)) @@ -629,7 +631,7 @@ class TestCoverMQTT(unittest.TestCase): 'cover.test', 'state-topic', 'command-topic', None, 'tilt-command-topic', 'tilt-status-topic', 0, False, 'OPEN', 'CLOSE', 'OPEN', 'CLOSE', 'STOP', None, None, - False, None, 180, 80, 80, 180, False, False, None, None) + False, None, 180, 80, 80, 180, False, False, None, None, None) self.assertEqual(120, mqtt_cover.find_in_range_from_percent(40)) @@ -639,7 +641,7 @@ class TestCoverMQTT(unittest.TestCase): 'cover.test', 'state-topic', 'command-topic', None, 'tilt-command-topic', 'tilt-status-topic', 0, False, 'OPEN', 'CLOSE', 'OPEN', 'CLOSE', 'STOP', None, None, - False, None, 100, 0, 0, 100, False, True, None, None) + False, None, 100, 0, 0, 100, False, True, None, None, None) self.assertEqual(44, mqtt_cover.find_in_range_from_percent(56)) @@ -649,7 +651,7 @@ class TestCoverMQTT(unittest.TestCase): 'cover.test', 'state-topic', 'command-topic', None, 'tilt-command-topic', 'tilt-status-topic', 0, False, 'OPEN', 'CLOSE', 'OPEN', 'CLOSE', 'STOP', None, None, - False, None, 180, 80, 80, 180, False, True, None, None) + False, None, 180, 80, 80, 180, False, True, None, None, None) self.assertEqual(120, mqtt_cover.find_in_range_from_percent(60)) @@ -722,3 +724,24 @@ class TestCoverMQTT(unittest.TestCase): state = self.hass.states.get('cover.test') self.assertEqual(STATE_UNAVAILABLE, state.state) + + +async def test_discovery_removal_cover(hass, mqtt_mock, caplog): + """Test removal of discovered cover.""" + await async_start(hass, 'homeassistant', {}) + data = ( + '{ "name": "Beer",' + ' "command_topic": "test_topic" }' + ) + async_fire_mqtt_message(hass, 'homeassistant/cover/bla/config', + data) + await hass.async_block_till_done() + state = hass.states.get('cover.beer') + assert state is not None + assert state.name == 'Beer' + async_fire_mqtt_message(hass, 'homeassistant/cover/bla/config', + '') + await hass.async_block_till_done() + await hass.async_block_till_done() + state = hass.states.get('cover.beer') + assert state is None From 7de0e1e39a8eafd0f8458588488cb416908e22aa Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 25 Sep 2018 20:47:22 +0200 Subject: [PATCH 049/247] Add executor job (#16853) --- homeassistant/components/lovelace/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/lovelace/__init__.py b/homeassistant/components/lovelace/__init__.py index 4336f50def4..eba69159048 100644 --- a/homeassistant/components/lovelace/__init__.py +++ b/homeassistant/components/lovelace/__init__.py @@ -34,7 +34,7 @@ async def websocket_lovelace_config(hass, connection, msg): """Send lovelace UI config over websocket config.""" error = None try: - config = await hass.async_add_job( + config = await hass.async_add_executor_job( load_yaml, hass.config.path('ui-lovelace.yaml')) message = websocket_api.result_message( msg['id'], config From 5a22e7d2115ea1ae25278df2e62bf98ee8b0e440 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 25 Sep 2018 20:47:51 +0200 Subject: [PATCH 050/247] Fail if dirty (#16839) * Fail if dirty * Update check_dirty * Update text * Fix comment * Add -e * Update dirty script --- script/check_dirty | 7 +++++++ tox.ini | 2 ++ 2 files changed, 9 insertions(+) create mode 100755 script/check_dirty diff --git a/script/check_dirty b/script/check_dirty new file mode 100755 index 00000000000..94db657a542 --- /dev/null +++ b/script/check_dirty @@ -0,0 +1,7 @@ +#!/bin/bash +[[ -z $(git ls-files --others --exclude-standard) ]] && exit 0 + +echo -e '\n***** ERROR\nTests are leaving files behind. Please update the tests to avoid writing any files:' +git ls-files --others --exclude-standard +echo +exit 1 diff --git a/tox.ini b/tox.ini index 60dacd5d8cb..dcfb209ef3a 100644 --- a/tox.ini +++ b/tox.ini @@ -13,6 +13,7 @@ whitelist_externals = /usr/bin/env install_command = /usr/bin/env LANG=C.UTF-8 pip install {opts} {packages} commands = pytest --timeout=9 --duration=10 {posargs} + {toxinidir}/script/check_dirty deps = -r{toxinidir}/requirements_test_all.txt -c{toxinidir}/homeassistant/package_constraints.txt @@ -29,6 +30,7 @@ whitelist_externals = /usr/bin/env install_command = /usr/bin/env LANG=C.UTF-8 pip install {opts} {packages} commands = pytest --timeout=9 --duration=10 --cov --cov-report= {posargs} + {toxinidir}/script/check_dirty deps = -r{toxinidir}/requirements_test_all.txt -c{toxinidir}/homeassistant/package_constraints.txt From 8e311686d0cfeafa04345eeaa670f442ce43d2d1 Mon Sep 17 00:00:00 2001 From: Jedmeng Date: Wed, 26 Sep 2018 02:49:37 +0800 Subject: [PATCH 051/247] Add support for Opple light (#16765) * Add support for Opple light * Update docstring * review fix * review fix * review fix --- .coveragerc | 1 + homeassistant/components/light/opple.py | 147 ++++++++++++++++++++++++ requirements_all.txt | 3 + 3 files changed, 151 insertions(+) create mode 100644 homeassistant/components/light/opple.py diff --git a/.coveragerc b/.coveragerc index defbc0022bd..a935564791a 100644 --- a/.coveragerc +++ b/.coveragerc @@ -510,6 +510,7 @@ omit = homeassistant/components/light/lw12wifi.py homeassistant/components/light/mystrom.py homeassistant/components/light/nanoleaf_aurora.py + homeassistant/components/light/opple.py homeassistant/components/light/osramlightify.py homeassistant/components/light/piglow.py homeassistant/components/light/rpi_gpio_pwm.py diff --git a/homeassistant/components/light/opple.py b/homeassistant/components/light/opple.py new file mode 100644 index 00000000000..66850d04406 --- /dev/null +++ b/homeassistant/components/light/opple.py @@ -0,0 +1,147 @@ +""" +Support for the Opple light. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/light.opple/ +""" + +import logging + +import voluptuous as vol + +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, + SUPPORT_COLOR_TEMP, Light) +from homeassistant.const import CONF_HOST, CONF_NAME +import homeassistant.helpers.config_validation as cv +from homeassistant.util.color import \ + color_temperature_kelvin_to_mired as kelvin_to_mired +from homeassistant.util.color import \ + color_temperature_mired_to_kelvin as mired_to_kelvin + +REQUIREMENTS = ['pyoppleio==1.0.5'] + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_NAME = "opple light" + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string +}) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up Opple light platform.""" + name = config[CONF_NAME] + host = config[CONF_HOST] + entity = OppleLight(name, host) + add_entities([entity]) + + _LOGGER.debug("Init light %s %s", host, entity.unique_id) + + +class OppleLight(Light): + """Opple light device.""" + + def __init__(self, name, host): + """Initialize an Opple light.""" + from pyoppleio.OppleLightDevice import OppleLightDevice + self._device = OppleLightDevice(host) + + self._name = name + self._is_on = None + self._brightness = None + self._color_temp = None + + @property + def available(self): + """Return True if light is available.""" + return self._device.is_online + + @property + def unique_id(self): + """Return unique ID for light.""" + return self._device.mac + + @property + def name(self): + """Return the display name of this light.""" + return self._name + + @property + def is_on(self): + """Return true if light is on.""" + return self._is_on + + @property + def brightness(self): + """Return the brightness of the light.""" + return self._brightness + + @property + def color_temp(self): + """Return the color temperature of this light.""" + return kelvin_to_mired(self._color_temp) + + @property + def min_mireds(self): + """Return minimum supported color temperature.""" + return 175 + + @property + def max_mireds(self): + """Return maximum supported color temperature.""" + return 333 + + @property + def supported_features(self): + """Flag supported features.""" + return SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP + + def turn_on(self, **kwargs): + """Instruct the light to turn on.""" + _LOGGER.debug("Turn on light %s %s", self._device.ip, kwargs) + if not self.is_on: + self._device.power_on = True + + if ATTR_BRIGHTNESS in kwargs and \ + self.brightness != kwargs[ATTR_BRIGHTNESS]: + self._device.brightness = kwargs[ATTR_BRIGHTNESS] + + if ATTR_COLOR_TEMP in kwargs and \ + self.brightness != kwargs[ATTR_COLOR_TEMP]: + color_temp = mired_to_kelvin(kwargs[ATTR_COLOR_TEMP]) + self._device.color_temperature = color_temp + + def turn_off(self, **kwargs): + """Instruct the light to turn off.""" + self._device.power_on = False + _LOGGER.debug("Turn off light %s", self._device.ip) + + def update(self): + """Synchronize state with light.""" + prev_available = self.available + self._device.update() + + if prev_available == self.available and \ + self._is_on == self._device.power_on and \ + self._brightness == self._device.brightness and \ + self._color_temp == self._device.color_temperature: + return + + if not self.available: + _LOGGER.debug("Light %s is offline", self._device.ip) + return + + self._is_on = self._device.power_on + self._brightness = self._device.brightness + self._color_temp = self._device.color_temperature + + if not self.is_on: + _LOGGER.debug("Update light %s success: power off", + self._device.ip) + else: + _LOGGER.debug("Update light %s success: power on brightness %s " + "color temperature %s", + self._device.ip, self._brightness, self._color_temp) diff --git a/requirements_all.txt b/requirements_all.txt index 40d4442dee2..b6f9dd4875e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1014,6 +1014,9 @@ pynx584==0.4 # homeassistant.components.openuv pyopenuv==1.0.4 +# homeassistant.components.light.opple +pyoppleio==1.0.5 + # homeassistant.components.iota pyota==2.0.5 From 7eaf8640d09b792f70b830ca82fe2dbc5eb267e3 Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Tue, 25 Sep 2018 22:32:05 +0200 Subject: [PATCH 052/247] Update cover tests (#16832) * Update test_group * Update test_command_line * Update test_demo * Update test_mqtt * Update test_template * Remove cover service call helpers --- homeassistant/components/cover/__init__.py | 58 --- tests/components/cover/test_command_line.py | 126 ++--- tests/components/cover/test_demo.py | 303 ++++++----- tests/components/cover/test_group.py | 551 ++++++++++---------- tests/components/cover/test_mqtt.py | 65 ++- tests/components/cover/test_template.py | 81 ++- 6 files changed, 595 insertions(+), 589 deletions(-) diff --git a/homeassistant/components/cover/__init__.py b/homeassistant/components/cover/__init__.py index e9a33c27d34..ec11b139f6b 100644 --- a/homeassistant/components/cover/__init__.py +++ b/homeassistant/components/cover/__init__.py @@ -81,64 +81,6 @@ def is_closed(hass, entity_id=None): return hass.states.is_state(entity_id, STATE_CLOSED) -@bind_hass -def open_cover(hass, entity_id=None): - """Open all or specified cover.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else None - hass.services.call(DOMAIN, SERVICE_OPEN_COVER, data) - - -@bind_hass -def close_cover(hass, entity_id=None): - """Close all or specified cover.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else None - hass.services.call(DOMAIN, SERVICE_CLOSE_COVER, data) - - -@bind_hass -def set_cover_position(hass, position, entity_id=None): - """Move to specific position all or specified cover.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} - data[ATTR_POSITION] = position - hass.services.call(DOMAIN, SERVICE_SET_COVER_POSITION, data) - - -@bind_hass -def stop_cover(hass, entity_id=None): - """Stop all or specified cover.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else None - hass.services.call(DOMAIN, SERVICE_STOP_COVER, data) - - -@bind_hass -def open_cover_tilt(hass, entity_id=None): - """Open all or specified cover tilt.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else None - hass.services.call(DOMAIN, SERVICE_OPEN_COVER_TILT, data) - - -@bind_hass -def close_cover_tilt(hass, entity_id=None): - """Close all or specified cover tilt.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else None - hass.services.call(DOMAIN, SERVICE_CLOSE_COVER_TILT, data) - - -@bind_hass -def set_cover_tilt_position(hass, tilt_position, entity_id=None): - """Move to specific tilt position all or specified cover.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} - data[ATTR_TILT_POSITION] = tilt_position - hass.services.call(DOMAIN, SERVICE_SET_COVER_TILT_POSITION, data) - - -@bind_hass -def stop_cover_tilt(hass, entity_id=None): - """Stop all or specified cover tilt.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else None - hass.services.call(DOMAIN, SERVICE_STOP_COVER_TILT, data) - - async def async_setup(hass, config): """Track states and offer events for covers.""" component = hass.data[DOMAIN] = EntityComponent( diff --git a/tests/components/cover/test_command_line.py b/tests/components/cover/test_command_line.py index 346c3f94683..0e03539d58c 100644 --- a/tests/components/cover/test_command_line.py +++ b/tests/components/cover/test_command_line.py @@ -1,87 +1,75 @@ """The tests the cover command line platform.""" - import os import tempfile -import unittest from unittest import mock -from homeassistant.setup import setup_component -import homeassistant.components.cover as cover -from homeassistant.components.cover import ( - command_line as cmd_rs) +import pytest -from tests.common import get_test_home_assistant +from homeassistant.components.cover import DOMAIN +import homeassistant.components.cover.command_line as cmd_rs +from homeassistant.const import ( + ATTR_ENTITY_ID, SERVICE_CLOSE_COVER, SERVICE_OPEN_COVER, + SERVICE_STOP_COVER) +from homeassistant.setup import async_setup_component -class TestCommandCover(unittest.TestCase): - """Test the cover command line platform.""" +@pytest.fixture +def rs(hass): + """Return CommandCover instance.""" + return cmd_rs.CommandCover(hass, 'foo', 'command_open', 'command_close', + 'command_stop', 'command_state', None) - def setup_method(self, method): - """Set up things to be run when tests are started.""" - self.hass = get_test_home_assistant() - self.rs = cmd_rs.CommandCover(self.hass, 'foo', - 'command_open', 'command_close', - 'command_stop', 'command_state', - None) - def teardown_method(self, method): - """Stop down everything that was started.""" - self.hass.stop() +def test_should_poll_new(rs): + """Test the setting of polling.""" + assert rs.should_poll is True + rs._command_state = None + assert rs.should_poll is False - def test_should_poll(self): - """Test the setting of polling.""" - self.assertTrue(self.rs.should_poll) - self.rs._command_state = None - self.assertFalse(self.rs.should_poll) - def test_query_state_value(self): - """Test with state value.""" - with mock.patch('subprocess.check_output') as mock_run: - mock_run.return_value = b' foo bar ' - result = self.rs._query_state_value('runme') - self.assertEqual('foo bar', result) - self.assertEqual(mock_run.call_count, 1) - self.assertEqual( - mock_run.call_args, mock.call('runme', shell=True) - ) +def test_query_state_value(rs): + """Test with state value.""" + with mock.patch('subprocess.check_output') as mock_run: + mock_run.return_value = b' foo bar ' + result = rs._query_state_value('runme') + assert 'foo bar' == result + assert mock_run.call_count == 1 + assert mock_run.call_args == mock.call('runme', shell=True) - def test_state_value(self): - """Test with state value.""" - with tempfile.TemporaryDirectory() as tempdirname: - path = os.path.join(tempdirname, 'cover_status') - test_cover = { - 'command_state': 'cat {}'.format(path), - 'command_open': 'echo 1 > {}'.format(path), - 'command_close': 'echo 1 > {}'.format(path), - 'command_stop': 'echo 0 > {}'.format(path), - 'value_template': '{{ value }}' - } - self.assertTrue(setup_component(self.hass, cover.DOMAIN, { - 'cover': { - 'platform': 'command_line', - 'covers': { - 'test': test_cover - } + +async def test_state_value(hass): + """Test with state value.""" + with tempfile.TemporaryDirectory() as tempdirname: + path = os.path.join(tempdirname, 'cover_status') + test_cover = { + 'command_state': 'cat {}'.format(path), + 'command_open': 'echo 1 > {}'.format(path), + 'command_close': 'echo 1 > {}'.format(path), + 'command_stop': 'echo 0 > {}'.format(path), + 'value_template': '{{ value }}' + } + assert await async_setup_component(hass, DOMAIN, { + 'cover': { + 'platform': 'command_line', + 'covers': { + 'test': test_cover } - })) + } + }) is True - state = self.hass.states.get('cover.test') - self.assertEqual('unknown', state.state) + assert 'unknown' == hass.states.get('cover.test').state - cover.open_cover(self.hass, 'cover.test') - self.hass.block_till_done() + await hass.services.async_call( + DOMAIN, SERVICE_OPEN_COVER, + {ATTR_ENTITY_ID: 'cover.test'}, blocking=True) + assert 'open' == hass.states.get('cover.test').state - state = self.hass.states.get('cover.test') - self.assertEqual('open', state.state) + await hass.services.async_call( + DOMAIN, SERVICE_CLOSE_COVER, + {ATTR_ENTITY_ID: 'cover.test'}, blocking=True) + assert 'open' == hass.states.get('cover.test').state - cover.close_cover(self.hass, 'cover.test') - self.hass.block_till_done() - - state = self.hass.states.get('cover.test') - self.assertEqual('open', state.state) - - cover.stop_cover(self.hass, 'cover.test') - self.hass.block_till_done() - - state = self.hass.states.get('cover.test') - self.assertEqual('closed', state.state) + await hass.services.async_call( + DOMAIN, SERVICE_STOP_COVER, + {ATTR_ENTITY_ID: 'cover.test'}, blocking=True) + assert 'closed' == hass.states.get('cover.test').state diff --git a/tests/components/cover/test_demo.py b/tests/components/cover/test_demo.py index 65aa9a9b9ef..011928f851a 100644 --- a/tests/components/cover/test_demo.py +++ b/tests/components/cover/test_demo.py @@ -1,158 +1,181 @@ """The tests for the Demo cover platform.""" -import unittest from datetime import timedelta + +import pytest + +from homeassistant.components.cover import ( + ATTR_POSITION, ATTR_TILT_POSITION, DOMAIN) +from homeassistant.const import ( + ATTR_ENTITY_ID, SERVICE_CLOSE_COVER, SERVICE_CLOSE_COVER_TILT, + SERVICE_OPEN_COVER, SERVICE_OPEN_COVER_TILT, SERVICE_SET_COVER_POSITION, + SERVICE_SET_COVER_TILT_POSITION, SERVICE_STOP_COVER, + SERVICE_STOP_COVER_TILT) +from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from homeassistant.setup import setup_component -from homeassistant.components import cover -from tests.common import get_test_home_assistant, fire_time_changed +from tests.common import assert_setup_component, async_fire_time_changed +CONFIG = {'cover': {'platform': 'demo'}} ENTITY_COVER = 'cover.living_room_window' -class TestCoverDemo(unittest.TestCase): - """Test the Demo cover.""" +@pytest.fixture +async def setup_comp(hass): + """Set up demo cover component.""" + with assert_setup_component(1, DOMAIN): + await async_setup_component(hass, DOMAIN, CONFIG) - def setUp(self): # pylint: disable=invalid-name - """Set up things to be run when tests are started.""" - self.hass = get_test_home_assistant() - self.assertTrue(setup_component(self.hass, cover.DOMAIN, {'cover': { - 'platform': 'demo', - }})) - def tearDown(self): # pylint: disable=invalid-name - """Stop down everything that was started.""" - self.hass.stop() +async def test_supported_features(hass, setup_comp): + """Test cover supported features.""" + state = hass.states.get('cover.garage_door') + assert 3 == state.attributes.get('supported_features') + state = hass.states.get('cover.kitchen_window') + assert 11 == state.attributes.get('supported_features') + state = hass.states.get('cover.hall_window') + assert 15 == state.attributes.get('supported_features') + state = hass.states.get('cover.living_room_window') + assert 255 == state.attributes.get('supported_features') - def test_supported_features(self): - """Test cover supported features.""" - state = self.hass.states.get('cover.garage_door') - self.assertEqual(3, state.attributes.get('supported_features')) - state = self.hass.states.get('cover.kitchen_window') - self.assertEqual(11, state.attributes.get('supported_features')) - state = self.hass.states.get('cover.hall_window') - self.assertEqual(15, state.attributes.get('supported_features')) - state = self.hass.states.get('cover.living_room_window') - self.assertEqual(255, state.attributes.get('supported_features')) - def test_close_cover(self): - """Test closing the cover.""" - state = self.hass.states.get(ENTITY_COVER) - self.assertEqual(state.state, 'open') - self.assertEqual(70, state.attributes.get('current_position')) - cover.close_cover(self.hass, ENTITY_COVER) - self.hass.block_till_done() - state = self.hass.states.get(ENTITY_COVER) - self.assertEqual(state.state, 'closing') - for _ in range(7): - future = dt_util.utcnow() + timedelta(seconds=1) - fire_time_changed(self.hass, future) - self.hass.block_till_done() +async def test_close_cover(hass, setup_comp): + """Test closing the cover.""" + state = hass.states.get(ENTITY_COVER) + assert state.state == 'open' + assert 70 == state.attributes.get('current_position') - state = self.hass.states.get(ENTITY_COVER) - self.assertEqual(state.state, 'closed') - self.assertEqual(0, state.attributes.get('current_position')) - - def test_open_cover(self): - """Test opening the cover.""" - state = self.hass.states.get(ENTITY_COVER) - self.assertEqual(state.state, 'open') - self.assertEqual(70, state.attributes.get('current_position')) - cover.open_cover(self.hass, ENTITY_COVER) - self.hass.block_till_done() - state = self.hass.states.get(ENTITY_COVER) - self.assertEqual(state.state, 'opening') - for _ in range(7): - future = dt_util.utcnow() + timedelta(seconds=1) - fire_time_changed(self.hass, future) - self.hass.block_till_done() - - state = self.hass.states.get(ENTITY_COVER) - self.assertEqual(state.state, 'open') - self.assertEqual(100, state.attributes.get('current_position')) - - def test_set_cover_position(self): - """Test moving the cover to a specific position.""" - state = self.hass.states.get(ENTITY_COVER) - self.assertEqual(70, state.attributes.get('current_position')) - cover.set_cover_position(self.hass, 10, ENTITY_COVER) - self.hass.block_till_done() - for _ in range(6): - future = dt_util.utcnow() + timedelta(seconds=1) - fire_time_changed(self.hass, future) - self.hass.block_till_done() - - state = self.hass.states.get(ENTITY_COVER) - self.assertEqual(10, state.attributes.get('current_position')) - - def test_stop_cover(self): - """Test stopping the cover.""" - state = self.hass.states.get(ENTITY_COVER) - self.assertEqual(70, state.attributes.get('current_position')) - cover.open_cover(self.hass, ENTITY_COVER) - self.hass.block_till_done() + await hass.services.async_call( + DOMAIN, SERVICE_CLOSE_COVER, + {ATTR_ENTITY_ID: ENTITY_COVER}, blocking=True) + state = hass.states.get(ENTITY_COVER) + assert state.state == 'closing' + for _ in range(7): future = dt_util.utcnow() + timedelta(seconds=1) - fire_time_changed(self.hass, future) - self.hass.block_till_done() - cover.stop_cover(self.hass, ENTITY_COVER) - self.hass.block_till_done() - fire_time_changed(self.hass, future) - state = self.hass.states.get(ENTITY_COVER) - self.assertEqual(80, state.attributes.get('current_position')) + async_fire_time_changed(hass, future) + await hass.async_block_till_done() - def test_close_cover_tilt(self): - """Test closing the cover tilt.""" - state = self.hass.states.get(ENTITY_COVER) - self.assertEqual(50, state.attributes.get('current_tilt_position')) - cover.close_cover_tilt(self.hass, ENTITY_COVER) - self.hass.block_till_done() - for _ in range(7): - future = dt_util.utcnow() + timedelta(seconds=1) - fire_time_changed(self.hass, future) - self.hass.block_till_done() + state = hass.states.get(ENTITY_COVER) + assert state.state == 'closed' + assert 0 == state.attributes.get('current_position') - state = self.hass.states.get(ENTITY_COVER) - self.assertEqual(0, state.attributes.get('current_tilt_position')) - def test_open_cover_tilt(self): - """Test opening the cover tilt.""" - state = self.hass.states.get(ENTITY_COVER) - self.assertEqual(50, state.attributes.get('current_tilt_position')) - cover.open_cover_tilt(self.hass, ENTITY_COVER) - self.hass.block_till_done() - for _ in range(7): - future = dt_util.utcnow() + timedelta(seconds=1) - fire_time_changed(self.hass, future) - self.hass.block_till_done() - - state = self.hass.states.get(ENTITY_COVER) - self.assertEqual(100, state.attributes.get('current_tilt_position')) - - def test_set_cover_tilt_position(self): - """Test moving the cover til to a specific position.""" - state = self.hass.states.get(ENTITY_COVER) - self.assertEqual(50, state.attributes.get('current_tilt_position')) - cover.set_cover_tilt_position(self.hass, 90, ENTITY_COVER) - self.hass.block_till_done() - for _ in range(7): - future = dt_util.utcnow() + timedelta(seconds=1) - fire_time_changed(self.hass, future) - self.hass.block_till_done() - - state = self.hass.states.get(ENTITY_COVER) - self.assertEqual(90, state.attributes.get('current_tilt_position')) - - def test_stop_cover_tilt(self): - """Test stopping the cover tilt.""" - state = self.hass.states.get(ENTITY_COVER) - self.assertEqual(50, state.attributes.get('current_tilt_position')) - cover.close_cover_tilt(self.hass, ENTITY_COVER) - self.hass.block_till_done() +async def test_open_cover(hass, setup_comp): + """Test opening the cover.""" + state = hass.states.get(ENTITY_COVER) + assert state.state == 'open' + assert 70 == state.attributes.get('current_position') + await hass.services.async_call( + DOMAIN, SERVICE_OPEN_COVER, + {ATTR_ENTITY_ID: ENTITY_COVER}, blocking=True) + state = hass.states.get(ENTITY_COVER) + assert state.state == 'opening' + for _ in range(7): future = dt_util.utcnow() + timedelta(seconds=1) - fire_time_changed(self.hass, future) - self.hass.block_till_done() - cover.stop_cover_tilt(self.hass, ENTITY_COVER) - self.hass.block_till_done() - fire_time_changed(self.hass, future) - state = self.hass.states.get(ENTITY_COVER) - self.assertEqual(40, state.attributes.get('current_tilt_position')) + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + state = hass.states.get(ENTITY_COVER) + assert state.state == 'open' + assert 100 == state.attributes.get('current_position') + + +async def test_set_cover_position(hass, setup_comp): + """Test moving the cover to a specific position.""" + state = hass.states.get(ENTITY_COVER) + assert 70 == state.attributes.get('current_position') + await hass.services.async_call( + DOMAIN, SERVICE_SET_COVER_POSITION, + {ATTR_ENTITY_ID: ENTITY_COVER, ATTR_POSITION: 10}, blocking=True) + for _ in range(6): + future = dt_util.utcnow() + timedelta(seconds=1) + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + state = hass.states.get(ENTITY_COVER) + assert 10 == state.attributes.get('current_position') + + +async def test_stop_cover(hass, setup_comp): + """Test stopping the cover.""" + state = hass.states.get(ENTITY_COVER) + assert 70 == state.attributes.get('current_position') + await hass.services.async_call( + DOMAIN, SERVICE_OPEN_COVER, + {ATTR_ENTITY_ID: ENTITY_COVER}, blocking=True) + future = dt_util.utcnow() + timedelta(seconds=1) + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + await hass.services.async_call( + DOMAIN, SERVICE_STOP_COVER, + {ATTR_ENTITY_ID: ENTITY_COVER}, blocking=True) + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + state = hass.states.get(ENTITY_COVER) + assert 80 == state.attributes.get('current_position') + + +async def test_close_cover_tilt(hass, setup_comp): + """Test closing the cover tilt.""" + state = hass.states.get(ENTITY_COVER) + assert 50 == state.attributes.get('current_tilt_position') + await hass.services.async_call( + DOMAIN, SERVICE_CLOSE_COVER_TILT, + {ATTR_ENTITY_ID: ENTITY_COVER}, blocking=True) + for _ in range(7): + future = dt_util.utcnow() + timedelta(seconds=1) + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + state = hass.states.get(ENTITY_COVER) + assert 0 == state.attributes.get('current_tilt_position') + + +async def test_open_cover_tilt(hass, setup_comp): + """Test opening the cover tilt.""" + state = hass.states.get(ENTITY_COVER) + assert 50 == state.attributes.get('current_tilt_position') + await hass.services.async_call( + DOMAIN, SERVICE_OPEN_COVER_TILT, + {ATTR_ENTITY_ID: ENTITY_COVER}, blocking=True) + for _ in range(7): + future = dt_util.utcnow() + timedelta(seconds=1) + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + state = hass.states.get(ENTITY_COVER) + assert 100 == state.attributes.get('current_tilt_position') + + +async def test_set_cover_tilt_position(hass, setup_comp): + """Test moving the cover til to a specific position.""" + state = hass.states.get(ENTITY_COVER) + assert 50 == state.attributes.get('current_tilt_position') + await hass.services.async_call( + DOMAIN, SERVICE_SET_COVER_TILT_POSITION, + {ATTR_ENTITY_ID: ENTITY_COVER, ATTR_TILT_POSITION: 90}, blocking=True) + for _ in range(7): + future = dt_util.utcnow() + timedelta(seconds=1) + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + state = hass.states.get(ENTITY_COVER) + assert 90 == state.attributes.get('current_tilt_position') + + +async def test_stop_cover_tilt(hass, setup_comp): + """Test stopping the cover tilt.""" + state = hass.states.get(ENTITY_COVER) + assert 50 == state.attributes.get('current_tilt_position') + await hass.services.async_call( + DOMAIN, SERVICE_CLOSE_COVER_TILT, + {ATTR_ENTITY_ID: ENTITY_COVER}, blocking=True) + future = dt_util.utcnow() + timedelta(seconds=1) + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + await hass.services.async_call( + DOMAIN, SERVICE_STOP_COVER_TILT, + {ATTR_ENTITY_ID: ENTITY_COVER}, blocking=True) + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + state = hass.states.get(ENTITY_COVER) + assert 40 == state.attributes.get('current_tilt_position') diff --git a/tests/components/cover/test_group.py b/tests/components/cover/test_group.py index 028845983a0..2211c8c77bc 100644 --- a/tests/components/cover/test_group.py +++ b/tests/components/cover/test_group.py @@ -1,19 +1,23 @@ """The tests for the group cover platform.""" - -import unittest from datetime import timedelta -import homeassistant.util.dt as dt_util -from homeassistant import setup -from homeassistant.components import cover +import pytest + from homeassistant.components.cover import ( - ATTR_CURRENT_POSITION, ATTR_CURRENT_TILT_POSITION, DOMAIN) + ATTR_CURRENT_POSITION, ATTR_CURRENT_TILT_POSITION, ATTR_POSITION, + ATTR_TILT_POSITION, DOMAIN) from homeassistant.components.cover.group import DEFAULT_NAME from homeassistant.const import ( - ATTR_ASSUMED_STATE, ATTR_FRIENDLY_NAME, ATTR_SUPPORTED_FEATURES, - CONF_ENTITIES, STATE_OPEN, STATE_CLOSED) -from tests.common import ( - assert_setup_component, get_test_home_assistant, fire_time_changed) + ATTR_ASSUMED_STATE, ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, + ATTR_SUPPORTED_FEATURES, CONF_ENTITIES, + SERVICE_CLOSE_COVER, SERVICE_CLOSE_COVER_TILT, + SERVICE_OPEN_COVER, SERVICE_OPEN_COVER_TILT, SERVICE_SET_COVER_POSITION, + SERVICE_SET_COVER_TILT_POSITION, SERVICE_STOP_COVER, + SERVICE_STOP_COVER_TILT, STATE_OPEN, STATE_CLOSED) +from homeassistant.setup import async_setup_component +import homeassistant.util.dt as dt_util + +from tests.common import assert_setup_component, async_fire_time_changed COVER_GROUP = 'cover.cover_group' DEMO_COVER = 'cover.kitchen_window' @@ -31,320 +35,301 @@ CONFIG = { } -class TestMultiCover(unittest.TestCase): - """Test the group cover platform.""" +@pytest.fixture +async def setup_comp(hass): + """Set up group cover component.""" + with assert_setup_component(2, DOMAIN): + await async_setup_component(hass, DOMAIN, CONFIG) - def setUp(self): - """Set up things to be run when tests are started.""" - self.hass = get_test_home_assistant() - def tearDown(self): - """Stop down everything that was started.""" - self.hass.stop() +async def test_attributes(hass): + """Test handling of state attributes.""" + config = {DOMAIN: {'platform': 'group', CONF_ENTITIES: [ + DEMO_COVER, DEMO_COVER_POS, DEMO_COVER_TILT, DEMO_TILT]}} - def test_attributes(self): - """Test handling of state attributes.""" - config = {DOMAIN: {'platform': 'group', CONF_ENTITIES: [ - DEMO_COVER, DEMO_COVER_POS, DEMO_COVER_TILT, DEMO_TILT]}} + with assert_setup_component(1, DOMAIN): + await async_setup_component(hass, DOMAIN, config) - with assert_setup_component(1, DOMAIN): - assert setup.setup_component(self.hass, DOMAIN, config) + state = hass.states.get(COVER_GROUP) + assert state.state == STATE_CLOSED + assert state.attributes.get(ATTR_FRIENDLY_NAME) == DEFAULT_NAME + assert state.attributes.get(ATTR_ASSUMED_STATE) is None + assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == 0 + assert state.attributes.get(ATTR_CURRENT_POSITION) is None + assert state.attributes.get(ATTR_CURRENT_TILT_POSITION) is None - state = self.hass.states.get(COVER_GROUP) - attr = state.attributes - self.assertEqual(state.state, STATE_CLOSED) - self.assertEqual(attr.get(ATTR_FRIENDLY_NAME), DEFAULT_NAME) - self.assertEqual(attr.get(ATTR_ASSUMED_STATE), None) - self.assertEqual(attr.get(ATTR_SUPPORTED_FEATURES), 0) - self.assertEqual(attr.get(ATTR_CURRENT_POSITION), None) - self.assertEqual(attr.get(ATTR_CURRENT_TILT_POSITION), None) + # Add Entity that supports open / close / stop + hass.states.async_set( + DEMO_COVER, STATE_OPEN, {ATTR_SUPPORTED_FEATURES: 11}) + await hass.async_block_till_done() - # Add Entity that supports open / close / stop - self.hass.states.set( - DEMO_COVER, STATE_OPEN, {ATTR_SUPPORTED_FEATURES: 11}) - self.hass.block_till_done() + state = hass.states.get(COVER_GROUP) + assert state.state == STATE_OPEN + assert state.attributes.get(ATTR_ASSUMED_STATE) is None + assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == 11 + assert state.attributes.get(ATTR_CURRENT_POSITION) is None + assert state.attributes.get(ATTR_CURRENT_TILT_POSITION) is None - state = self.hass.states.get(COVER_GROUP) - attr = state.attributes - self.assertEqual(state.state, STATE_OPEN) - self.assertEqual(attr.get(ATTR_ASSUMED_STATE), None) - self.assertEqual(attr.get(ATTR_SUPPORTED_FEATURES), 11) - self.assertEqual(attr.get(ATTR_CURRENT_POSITION), None) - self.assertEqual(attr.get(ATTR_CURRENT_TILT_POSITION), None) + # Add Entity that supports set_cover_position + hass.states.async_set( + DEMO_COVER_POS, STATE_OPEN, + {ATTR_SUPPORTED_FEATURES: 4, ATTR_CURRENT_POSITION: 70}) + await hass.async_block_till_done() - # Add Entity that supports set_cover_position - self.hass.states.set( - DEMO_COVER_POS, STATE_OPEN, - {ATTR_SUPPORTED_FEATURES: 4, ATTR_CURRENT_POSITION: 70}) - self.hass.block_till_done() + state = hass.states.get(COVER_GROUP) + assert state.state == STATE_OPEN + assert state.attributes.get(ATTR_ASSUMED_STATE) is None + assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == 15 + assert state.attributes.get(ATTR_CURRENT_POSITION) == 70 + assert state.attributes.get(ATTR_CURRENT_TILT_POSITION) is None - state = self.hass.states.get(COVER_GROUP) - attr = state.attributes - self.assertEqual(state.state, STATE_OPEN) - self.assertEqual(attr.get(ATTR_ASSUMED_STATE), None) - self.assertEqual(attr.get(ATTR_SUPPORTED_FEATURES), 15) - self.assertEqual(attr.get(ATTR_CURRENT_POSITION), 70) - self.assertEqual(attr.get(ATTR_CURRENT_TILT_POSITION), None) + # Add Entity that supports open tilt / close tilt / stop tilt + hass.states.async_set( + DEMO_TILT, STATE_OPEN, {ATTR_SUPPORTED_FEATURES: 112}) + await hass.async_block_till_done() - # Add Entity that supports open tilt / close tilt / stop tilt - self.hass.states.set( - DEMO_TILT, STATE_OPEN, {ATTR_SUPPORTED_FEATURES: 112}) - self.hass.block_till_done() + state = hass.states.get(COVER_GROUP) + assert state.state == STATE_OPEN + assert state.attributes.get(ATTR_ASSUMED_STATE) is None + assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == 127 + assert state.attributes.get(ATTR_CURRENT_POSITION) == 70 + assert state.attributes.get(ATTR_CURRENT_TILT_POSITION) is None - state = self.hass.states.get(COVER_GROUP) - attr = state.attributes - self.assertEqual(state.state, STATE_OPEN) - self.assertEqual(attr.get(ATTR_ASSUMED_STATE), None) - self.assertEqual(attr.get(ATTR_SUPPORTED_FEATURES), 127) - self.assertEqual(attr.get(ATTR_CURRENT_POSITION), 70) - self.assertEqual(attr.get(ATTR_CURRENT_TILT_POSITION), None) + # Add Entity that supports set_tilt_position + hass.states.async_set( + DEMO_COVER_TILT, STATE_OPEN, + {ATTR_SUPPORTED_FEATURES: 128, ATTR_CURRENT_TILT_POSITION: 60}) + await hass.async_block_till_done() - # Add Entity that supports set_tilt_position - self.hass.states.set( - DEMO_COVER_TILT, STATE_OPEN, - {ATTR_SUPPORTED_FEATURES: 128, ATTR_CURRENT_TILT_POSITION: 60}) - self.hass.block_till_done() + state = hass.states.get(COVER_GROUP) + assert state.state == STATE_OPEN + assert state.attributes.get(ATTR_ASSUMED_STATE) is None + assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == 255 + assert state.attributes.get(ATTR_CURRENT_POSITION) == 70 + assert state.attributes.get(ATTR_CURRENT_TILT_POSITION) == 60 - state = self.hass.states.get(COVER_GROUP) - attr = state.attributes - self.assertEqual(state.state, STATE_OPEN) - self.assertEqual(attr.get(ATTR_ASSUMED_STATE), None) - self.assertEqual(attr.get(ATTR_SUPPORTED_FEATURES), 255) - self.assertEqual(attr.get(ATTR_CURRENT_POSITION), 70) - self.assertEqual(attr.get(ATTR_CURRENT_TILT_POSITION), 60) + # ### Test assumed state ### + # ########################## - # ### Test assumed state ### - # ########################## + # For covers + hass.states.async_set( + DEMO_COVER, STATE_OPEN, + {ATTR_SUPPORTED_FEATURES: 4, ATTR_CURRENT_POSITION: 100}) + await hass.async_block_till_done() - # For covers - self.hass.states.set( - DEMO_COVER, STATE_OPEN, - {ATTR_SUPPORTED_FEATURES: 4, ATTR_CURRENT_POSITION: 100}) - self.hass.block_till_done() + state = hass.states.get(COVER_GROUP) + assert state.state == STATE_OPEN + assert state.attributes.get(ATTR_ASSUMED_STATE) is True + assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == 244 + assert state.attributes.get(ATTR_CURRENT_POSITION) == 100 + assert state.attributes.get(ATTR_CURRENT_TILT_POSITION) == 60 - state = self.hass.states.get(COVER_GROUP) - attr = state.attributes - self.assertEqual(state.state, STATE_OPEN) - self.assertEqual(attr.get(ATTR_ASSUMED_STATE), True) - self.assertEqual(attr.get(ATTR_SUPPORTED_FEATURES), 244) - self.assertEqual(attr.get(ATTR_CURRENT_POSITION), 100) - self.assertEqual(attr.get(ATTR_CURRENT_TILT_POSITION), 60) + hass.states.async_remove(DEMO_COVER) + hass.states.async_remove(DEMO_COVER_POS) + await hass.async_block_till_done() - self.hass.states.remove(DEMO_COVER) - self.hass.block_till_done() - self.hass.states.remove(DEMO_COVER_POS) - self.hass.block_till_done() + state = hass.states.get(COVER_GROUP) + assert state.state == STATE_OPEN + assert state.attributes.get(ATTR_ASSUMED_STATE) is None + assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == 240 + assert state.attributes.get(ATTR_CURRENT_POSITION) is None + assert state.attributes.get(ATTR_CURRENT_TILT_POSITION) == 60 - state = self.hass.states.get(COVER_GROUP) - attr = state.attributes - self.assertEqual(state.state, STATE_OPEN) - self.assertEqual(attr.get(ATTR_ASSUMED_STATE), None) - self.assertEqual(attr.get(ATTR_SUPPORTED_FEATURES), 240) - self.assertEqual(attr.get(ATTR_CURRENT_POSITION), None) - self.assertEqual(attr.get(ATTR_CURRENT_TILT_POSITION), 60) + # For tilts + hass.states.async_set( + DEMO_TILT, STATE_OPEN, + {ATTR_SUPPORTED_FEATURES: 128, ATTR_CURRENT_TILT_POSITION: 100}) + await hass.async_block_till_done() - # For tilts - self.hass.states.set( - DEMO_TILT, STATE_OPEN, - {ATTR_SUPPORTED_FEATURES: 128, ATTR_CURRENT_TILT_POSITION: 100}) - self.hass.block_till_done() + state = hass.states.get(COVER_GROUP) + assert state.state == STATE_OPEN + assert state.attributes.get(ATTR_ASSUMED_STATE) is True + assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == 128 + assert state.attributes.get(ATTR_CURRENT_POSITION) is None + assert state.attributes.get(ATTR_CURRENT_TILT_POSITION) == 100 - state = self.hass.states.get(COVER_GROUP) - attr = state.attributes - self.assertEqual(state.state, STATE_OPEN) - self.assertEqual(attr.get(ATTR_ASSUMED_STATE), True) - self.assertEqual(attr.get(ATTR_SUPPORTED_FEATURES), 128) - self.assertEqual(attr.get(ATTR_CURRENT_POSITION), None) - self.assertEqual(attr.get(ATTR_CURRENT_TILT_POSITION), 100) + hass.states.async_remove(DEMO_COVER_TILT) + hass.states.async_set(DEMO_TILT, STATE_CLOSED) + await hass.async_block_till_done() - self.hass.states.remove(DEMO_COVER_TILT) - self.hass.states.set(DEMO_TILT, STATE_CLOSED) - self.hass.block_till_done() + state = hass.states.get(COVER_GROUP) + assert state.state == STATE_CLOSED + assert state.attributes.get(ATTR_ASSUMED_STATE) is None + assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == 0 + assert state.attributes.get(ATTR_CURRENT_POSITION) is None + assert state.attributes.get(ATTR_CURRENT_TILT_POSITION) is None - state = self.hass.states.get(COVER_GROUP) - attr = state.attributes - self.assertEqual(state.state, STATE_CLOSED) - self.assertEqual(attr.get(ATTR_ASSUMED_STATE), None) - self.assertEqual(attr.get(ATTR_SUPPORTED_FEATURES), 0) - self.assertEqual(attr.get(ATTR_CURRENT_POSITION), None) - self.assertEqual(attr.get(ATTR_CURRENT_TILT_POSITION), None) + hass.states.async_set( + DEMO_TILT, STATE_CLOSED, {ATTR_ASSUMED_STATE: True}) + await hass.async_block_till_done() - self.hass.states.set( - DEMO_TILT, STATE_CLOSED, {ATTR_ASSUMED_STATE: True}) - self.hass.block_till_done() + state = hass.states.get(COVER_GROUP) + assert state.attributes.get(ATTR_ASSUMED_STATE) is True - state = self.hass.states.get(COVER_GROUP) - attr = state.attributes - self.assertEqual(attr.get(ATTR_ASSUMED_STATE), True) - def test_open_covers(self): - """Test open cover function.""" - with assert_setup_component(2, DOMAIN): - assert setup.setup_component(self.hass, DOMAIN, CONFIG) - - cover.open_cover(self.hass, COVER_GROUP) - self.hass.block_till_done() - for _ in range(10): - future = dt_util.utcnow() + timedelta(seconds=1) - fire_time_changed(self.hass, future) - self.hass.block_till_done() - - state = self.hass.states.get(COVER_GROUP) - self.assertEqual(state.state, STATE_OPEN) - self.assertEqual(state.attributes.get(ATTR_CURRENT_POSITION), 100) - - self.assertEqual(self.hass.states.get(DEMO_COVER).state, STATE_OPEN) - self.assertEqual(self.hass.states.get(DEMO_COVER_POS) - .attributes.get(ATTR_CURRENT_POSITION), 100) - self.assertEqual(self.hass.states.get(DEMO_COVER_TILT) - .attributes.get(ATTR_CURRENT_POSITION), 100) - - def test_close_covers(self): - """Test close cover function.""" - with assert_setup_component(2, DOMAIN): - assert setup.setup_component(self.hass, DOMAIN, CONFIG) - - cover.close_cover(self.hass, COVER_GROUP) - self.hass.block_till_done() - for _ in range(10): - future = dt_util.utcnow() + timedelta(seconds=1) - fire_time_changed(self.hass, future) - self.hass.block_till_done() - - state = self.hass.states.get(COVER_GROUP) - self.assertEqual(state.state, STATE_CLOSED) - self.assertEqual(state.attributes.get(ATTR_CURRENT_POSITION), 0) - - self.assertEqual(self.hass.states.get(DEMO_COVER).state, STATE_CLOSED) - self.assertEqual(self.hass.states.get(DEMO_COVER_POS) - .attributes.get(ATTR_CURRENT_POSITION), 0) - self.assertEqual(self.hass.states.get(DEMO_COVER_TILT) - .attributes.get(ATTR_CURRENT_POSITION), 0) - - def test_stop_covers(self): - """Test stop cover function.""" - with assert_setup_component(2, DOMAIN): - assert setup.setup_component(self.hass, DOMAIN, CONFIG) - - cover.open_cover(self.hass, COVER_GROUP) - self.hass.block_till_done() +async def test_open_covers(hass, setup_comp): + """Test open cover function.""" + await hass.services.async_call( + DOMAIN, SERVICE_OPEN_COVER, + {ATTR_ENTITY_ID: COVER_GROUP}, blocking=True) + for _ in range(10): future = dt_util.utcnow() + timedelta(seconds=1) - fire_time_changed(self.hass, future) - self.hass.block_till_done() - cover.stop_cover(self.hass, COVER_GROUP) - self.hass.block_till_done() + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + state = hass.states.get(COVER_GROUP) + assert state.state == STATE_OPEN + assert state.attributes.get(ATTR_CURRENT_POSITION) == 100 + + assert hass.states.get(DEMO_COVER).state == STATE_OPEN + assert hass.states.get(DEMO_COVER_POS) \ + .attributes.get(ATTR_CURRENT_POSITION) == 100 + assert hass.states.get(DEMO_COVER_TILT) \ + .attributes.get(ATTR_CURRENT_POSITION) == 100 + + +async def test_close_covers(hass, setup_comp): + """Test close cover function.""" + await hass.services.async_call( + DOMAIN, SERVICE_CLOSE_COVER, + {ATTR_ENTITY_ID: COVER_GROUP}, blocking=True) + for _ in range(10): future = dt_util.utcnow() + timedelta(seconds=1) - fire_time_changed(self.hass, future) - self.hass.block_till_done() + async_fire_time_changed(hass, future) + await hass.async_block_till_done() - state = self.hass.states.get(COVER_GROUP) - self.assertEqual(state.state, STATE_OPEN) - self.assertEqual(state.attributes.get(ATTR_CURRENT_POSITION), 100) + state = hass.states.get(COVER_GROUP) + assert state.state == STATE_CLOSED + assert state.attributes.get(ATTR_CURRENT_POSITION) == 0 - self.assertEqual(self.hass.states.get(DEMO_COVER).state, STATE_OPEN) - self.assertEqual(self.hass.states.get(DEMO_COVER_POS) - .attributes.get(ATTR_CURRENT_POSITION), 20) - self.assertEqual(self.hass.states.get(DEMO_COVER_TILT) - .attributes.get(ATTR_CURRENT_POSITION), 80) + assert hass.states.get(DEMO_COVER).state == STATE_CLOSED + assert hass.states.get(DEMO_COVER_POS) \ + .attributes.get(ATTR_CURRENT_POSITION) == 0 + assert hass.states.get(DEMO_COVER_TILT) \ + .attributes.get(ATTR_CURRENT_POSITION) == 0 - def test_set_cover_position(self): - """Test set cover position function.""" - with assert_setup_component(2, DOMAIN): - assert setup.setup_component(self.hass, DOMAIN, CONFIG) - cover.set_cover_position(self.hass, 50, COVER_GROUP) - self.hass.block_till_done() - for _ in range(4): - future = dt_util.utcnow() + timedelta(seconds=1) - fire_time_changed(self.hass, future) - self.hass.block_till_done() +async def test_stop_covers(hass, setup_comp): + """Test stop cover function.""" + await hass.services.async_call( + DOMAIN, SERVICE_OPEN_COVER, + {ATTR_ENTITY_ID: COVER_GROUP}, blocking=True) + future = dt_util.utcnow() + timedelta(seconds=1) + async_fire_time_changed(hass, future) + await hass.async_block_till_done() - state = self.hass.states.get(COVER_GROUP) - self.assertEqual(state.state, STATE_OPEN) - self.assertEqual(state.attributes.get(ATTR_CURRENT_POSITION), 50) + await hass.services.async_call( + DOMAIN, SERVICE_STOP_COVER, + {ATTR_ENTITY_ID: COVER_GROUP}, blocking=True) + future = dt_util.utcnow() + timedelta(seconds=1) + async_fire_time_changed(hass, future) + await hass.async_block_till_done() - self.assertEqual(self.hass.states.get(DEMO_COVER).state, STATE_CLOSED) - self.assertEqual(self.hass.states.get(DEMO_COVER_POS) - .attributes.get(ATTR_CURRENT_POSITION), 50) - self.assertEqual(self.hass.states.get(DEMO_COVER_TILT) - .attributes.get(ATTR_CURRENT_POSITION), 50) + state = hass.states.get(COVER_GROUP) + assert state.state == STATE_OPEN + assert state.attributes.get(ATTR_CURRENT_POSITION) == 100 - def test_open_tilts(self): - """Test open tilt function.""" - with assert_setup_component(2, DOMAIN): - assert setup.setup_component(self.hass, DOMAIN, CONFIG) + assert hass.states.get(DEMO_COVER).state == STATE_OPEN + assert hass.states.get(DEMO_COVER_POS) \ + .attributes.get(ATTR_CURRENT_POSITION) == 20 + assert hass.states.get(DEMO_COVER_TILT) \ + .attributes.get(ATTR_CURRENT_POSITION) == 80 - cover.open_cover_tilt(self.hass, COVER_GROUP) - self.hass.block_till_done() - for _ in range(5): - future = dt_util.utcnow() + timedelta(seconds=1) - fire_time_changed(self.hass, future) - self.hass.block_till_done() - state = self.hass.states.get(COVER_GROUP) - self.assertEqual(state.state, STATE_OPEN) - self.assertEqual(state.attributes.get(ATTR_CURRENT_TILT_POSITION), 100) - - self.assertEqual(self.hass.states.get(DEMO_COVER_TILT) - .attributes.get(ATTR_CURRENT_TILT_POSITION), 100) - - def test_close_tilts(self): - """Test close tilt function.""" - with assert_setup_component(2, DOMAIN): - assert setup.setup_component(self.hass, DOMAIN, CONFIG) - - cover.close_cover_tilt(self.hass, COVER_GROUP) - self.hass.block_till_done() - for _ in range(5): - future = dt_util.utcnow() + timedelta(seconds=1) - fire_time_changed(self.hass, future) - self.hass.block_till_done() - - state = self.hass.states.get(COVER_GROUP) - self.assertEqual(state.state, STATE_OPEN) - self.assertEqual(state.attributes.get(ATTR_CURRENT_TILT_POSITION), 0) - - self.assertEqual(self.hass.states.get(DEMO_COVER_TILT) - .attributes.get(ATTR_CURRENT_TILT_POSITION), 0) - - def test_stop_tilts(self): - """Test stop tilts function.""" - with assert_setup_component(2, DOMAIN): - assert setup.setup_component(self.hass, DOMAIN, CONFIG) - - cover.open_cover_tilt(self.hass, COVER_GROUP) - self.hass.block_till_done() +async def test_set_cover_position(hass, setup_comp): + """Test set cover position function.""" + await hass.services.async_call( + DOMAIN, SERVICE_SET_COVER_POSITION, + {ATTR_ENTITY_ID: COVER_GROUP, ATTR_POSITION: 50}, blocking=True) + for _ in range(4): future = dt_util.utcnow() + timedelta(seconds=1) - fire_time_changed(self.hass, future) - self.hass.block_till_done() - cover.stop_cover_tilt(self.hass, COVER_GROUP) - self.hass.block_till_done() + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + state = hass.states.get(COVER_GROUP) + assert state.state == STATE_OPEN + assert state.attributes.get(ATTR_CURRENT_POSITION) == 50 + + assert hass.states.get(DEMO_COVER).state == STATE_CLOSED + assert hass.states.get(DEMO_COVER_POS) \ + .attributes.get(ATTR_CURRENT_POSITION) == 50 + assert hass.states.get(DEMO_COVER_TILT) \ + .attributes.get(ATTR_CURRENT_POSITION) == 50 + + +async def test_open_tilts(hass, setup_comp): + """Test open tilt function.""" + await hass.services.async_call( + DOMAIN, SERVICE_OPEN_COVER_TILT, + {ATTR_ENTITY_ID: COVER_GROUP}, blocking=True) + for _ in range(5): future = dt_util.utcnow() + timedelta(seconds=1) - fire_time_changed(self.hass, future) - self.hass.block_till_done() + async_fire_time_changed(hass, future) + await hass.async_block_till_done() - state = self.hass.states.get(COVER_GROUP) - self.assertEqual(state.state, STATE_OPEN) - self.assertEqual(state.attributes.get(ATTR_CURRENT_TILT_POSITION), 60) + state = hass.states.get(COVER_GROUP) + assert state.state == STATE_OPEN + assert state.attributes.get(ATTR_CURRENT_TILT_POSITION) == 100 - self.assertEqual(self.hass.states.get(DEMO_COVER_TILT) - .attributes.get(ATTR_CURRENT_TILT_POSITION), 60) + assert hass.states.get(DEMO_COVER_TILT) \ + .attributes.get(ATTR_CURRENT_TILT_POSITION) == 100 - def test_set_tilt_positions(self): - """Test set tilt position function.""" - with assert_setup_component(2, DOMAIN): - assert setup.setup_component(self.hass, DOMAIN, CONFIG) - cover.set_cover_tilt_position(self.hass, 80, COVER_GROUP) - self.hass.block_till_done() - for _ in range(3): - future = dt_util.utcnow() + timedelta(seconds=1) - fire_time_changed(self.hass, future) - self.hass.block_till_done() +async def test_close_tilts(hass, setup_comp): + """Test close tilt function.""" + await hass.services.async_call( + DOMAIN, SERVICE_CLOSE_COVER_TILT, + {ATTR_ENTITY_ID: COVER_GROUP}, blocking=True) + for _ in range(5): + future = dt_util.utcnow() + timedelta(seconds=1) + async_fire_time_changed(hass, future) + await hass.async_block_till_done() - state = self.hass.states.get(COVER_GROUP) - self.assertEqual(state.state, STATE_OPEN) - self.assertEqual(state.attributes.get(ATTR_CURRENT_TILT_POSITION), 80) + state = hass.states.get(COVER_GROUP) + assert state.state == STATE_OPEN + assert state.attributes.get(ATTR_CURRENT_TILT_POSITION) == 0 - self.assertEqual(self.hass.states.get(DEMO_COVER_TILT) - .attributes.get(ATTR_CURRENT_TILT_POSITION), 80) + assert hass.states.get(DEMO_COVER_TILT) \ + .attributes.get(ATTR_CURRENT_TILT_POSITION) == 0 + + +async def test_stop_tilts(hass, setup_comp): + """Test stop tilts function.""" + await hass.services.async_call( + DOMAIN, SERVICE_OPEN_COVER_TILT, + {ATTR_ENTITY_ID: COVER_GROUP}, blocking=True) + future = dt_util.utcnow() + timedelta(seconds=1) + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + await hass.services.async_call( + DOMAIN, SERVICE_STOP_COVER_TILT, + {ATTR_ENTITY_ID: COVER_GROUP}, blocking=True) + future = dt_util.utcnow() + timedelta(seconds=1) + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + state = hass.states.get(COVER_GROUP) + assert state.state == STATE_OPEN + assert state.attributes.get(ATTR_CURRENT_TILT_POSITION) == 60 + + assert hass.states.get(DEMO_COVER_TILT) \ + .attributes.get(ATTR_CURRENT_TILT_POSITION) == 60 + + +async def test_set_tilt_positions(hass, setup_comp): + """Test set tilt position function.""" + await hass.services.async_call( + DOMAIN, SERVICE_SET_COVER_TILT_POSITION, + {ATTR_ENTITY_ID: COVER_GROUP, ATTR_TILT_POSITION: 80}, blocking=True) + for _ in range(3): + future = dt_util.utcnow() + timedelta(seconds=1) + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + state = hass.states.get(COVER_GROUP) + assert state.state == STATE_OPEN + assert state.attributes.get(ATTR_CURRENT_TILT_POSITION) == 80 + + assert hass.states.get(DEMO_COVER_TILT) \ + .attributes.get(ATTR_CURRENT_TILT_POSITION) == 80 diff --git a/tests/components/cover/test_mqtt.py b/tests/components/cover/test_mqtt.py index e75178bf3d7..f1d3da9be84 100644 --- a/tests/components/cover/test_mqtt.py +++ b/tests/components/cover/test_mqtt.py @@ -1,12 +1,17 @@ """The tests for the MQTT cover platform.""" import unittest -from homeassistant.setup import setup_component -from homeassistant.const import STATE_OPEN, STATE_CLOSED, STATE_UNKNOWN, \ - STATE_UNAVAILABLE, ATTR_ASSUMED_STATE import homeassistant.components.cover as cover +from homeassistant.components.cover import (ATTR_POSITION, ATTR_TILT_POSITION) from homeassistant.components.cover.mqtt import MqttCover from homeassistant.components.mqtt.discovery import async_start +from homeassistant.const import ( + ATTR_ASSUMED_STATE, ATTR_ENTITY_ID, + SERVICE_CLOSE_COVER, SERVICE_CLOSE_COVER_TILT, SERVICE_OPEN_COVER, + SERVICE_OPEN_COVER_TILT, SERVICE_SET_COVER_POSITION, + SERVICE_SET_COVER_TILT_POSITION, SERVICE_STOP_COVER, + STATE_CLOSED, STATE_OPEN, STATE_UNAVAILABLE, STATE_UNKNOWN) +from homeassistant.setup import setup_component from tests.common import ( get_test_home_assistant, mock_mqtt_component, async_fire_mqtt_message, @@ -117,7 +122,9 @@ class TestCoverMQTT(unittest.TestCase): self.assertEqual(STATE_UNKNOWN, state.state) self.assertTrue(state.attributes.get(ATTR_ASSUMED_STATE)) - cover.open_cover(self.hass, 'cover.test') + self.hass.services.call( + cover.DOMAIN, SERVICE_OPEN_COVER, + {ATTR_ENTITY_ID: 'cover.test'}, blocking=True) self.hass.block_till_done() self.mock_publish.async_publish.assert_called_once_with( @@ -126,7 +133,9 @@ class TestCoverMQTT(unittest.TestCase): state = self.hass.states.get('cover.test') self.assertEqual(STATE_OPEN, state.state) - cover.close_cover(self.hass, 'cover.test') + self.hass.services.call( + cover.DOMAIN, SERVICE_CLOSE_COVER, + {ATTR_ENTITY_ID: 'cover.test'}, blocking=True) self.hass.block_till_done() self.mock_publish.async_publish.assert_called_once_with( @@ -149,7 +158,9 @@ class TestCoverMQTT(unittest.TestCase): state = self.hass.states.get('cover.test') self.assertEqual(STATE_UNKNOWN, state.state) - cover.open_cover(self.hass, 'cover.test') + self.hass.services.call( + cover.DOMAIN, SERVICE_OPEN_COVER, + {ATTR_ENTITY_ID: 'cover.test'}, blocking=True) self.hass.block_till_done() self.mock_publish.async_publish.assert_called_once_with( @@ -172,7 +183,9 @@ class TestCoverMQTT(unittest.TestCase): state = self.hass.states.get('cover.test') self.assertEqual(STATE_UNKNOWN, state.state) - cover.close_cover(self.hass, 'cover.test') + self.hass.services.call( + cover.DOMAIN, SERVICE_CLOSE_COVER, + {ATTR_ENTITY_ID: 'cover.test'}, blocking=True) self.hass.block_till_done() self.mock_publish.async_publish.assert_called_once_with( @@ -195,7 +208,9 @@ class TestCoverMQTT(unittest.TestCase): state = self.hass.states.get('cover.test') self.assertEqual(STATE_UNKNOWN, state.state) - cover.stop_cover(self.hass, 'cover.test') + self.hass.services.call( + cover.DOMAIN, SERVICE_STOP_COVER, + {ATTR_ENTITY_ID: 'cover.test'}, blocking=True) self.hass.block_till_done() self.mock_publish.async_publish.assert_called_once_with( @@ -297,7 +312,9 @@ class TestCoverMQTT(unittest.TestCase): } })) - cover.set_cover_position(self.hass, 100, 'cover.test') + self.hass.services.call( + cover.DOMAIN, SERVICE_SET_COVER_POSITION, + {ATTR_ENTITY_ID: 'cover.test', ATTR_POSITION: 100}, blocking=True) self.hass.block_till_done() self.mock_publish.async_publish.assert_called_once_with( @@ -318,7 +335,9 @@ class TestCoverMQTT(unittest.TestCase): } })) - cover.set_cover_position(self.hass, 62, 'cover.test') + self.hass.services.call( + cover.DOMAIN, SERVICE_SET_COVER_POSITION, + {ATTR_ENTITY_ID: 'cover.test', ATTR_POSITION: 62}, blocking=True) self.hass.block_till_done() self.mock_publish.async_publish.assert_called_once_with( @@ -403,14 +422,18 @@ class TestCoverMQTT(unittest.TestCase): } })) - cover.open_cover_tilt(self.hass, 'cover.test') + self.hass.services.call( + cover.DOMAIN, SERVICE_OPEN_COVER_TILT, + {ATTR_ENTITY_ID: 'cover.test'}, blocking=True) self.hass.block_till_done() self.mock_publish.async_publish.assert_called_once_with( 'tilt-command-topic', 100, 0, False) self.mock_publish.async_publish.reset_mock() - cover.close_cover_tilt(self.hass, 'cover.test') + self.hass.services.call( + cover.DOMAIN, SERVICE_CLOSE_COVER_TILT, + {ATTR_ENTITY_ID: 'cover.test'}, blocking=True) self.hass.block_till_done() self.mock_publish.async_publish.assert_called_once_with( @@ -435,14 +458,18 @@ class TestCoverMQTT(unittest.TestCase): } })) - cover.open_cover_tilt(self.hass, 'cover.test') + self.hass.services.call( + cover.DOMAIN, SERVICE_OPEN_COVER_TILT, + {ATTR_ENTITY_ID: 'cover.test'}, blocking=True) self.hass.block_till_done() self.mock_publish.async_publish.assert_called_once_with( 'tilt-command-topic', 400, 0, False) self.mock_publish.async_publish.reset_mock() - cover.close_cover_tilt(self.hass, 'cover.test') + self.hass.services.call( + cover.DOMAIN, SERVICE_CLOSE_COVER_TILT, + {ATTR_ENTITY_ID: 'cover.test'}, blocking=True) self.hass.block_till_done() self.mock_publish.async_publish.assert_called_once_with( @@ -542,7 +569,10 @@ class TestCoverMQTT(unittest.TestCase): } })) - cover.set_cover_tilt_position(self.hass, 50, 'cover.test') + self.hass.services.call( + cover.DOMAIN, SERVICE_SET_COVER_TILT_POSITION, + {ATTR_ENTITY_ID: 'cover.test', ATTR_TILT_POSITION: 50}, + blocking=True) self.hass.block_till_done() self.mock_publish.async_publish.assert_called_once_with( @@ -569,7 +599,10 @@ class TestCoverMQTT(unittest.TestCase): } })) - cover.set_cover_tilt_position(self.hass, 50, 'cover.test') + self.hass.services.call( + cover.DOMAIN, SERVICE_SET_COVER_TILT_POSITION, + {ATTR_ENTITY_ID: 'cover.test', ATTR_TILT_POSITION: 50}, + blocking=True) self.hass.block_till_done() self.mock_publish.async_publish.assert_called_once_with( diff --git a/tests/components/cover/test_template.py b/tests/components/cover/test_template.py index b786b463dad..3c820f1a0ac 100644 --- a/tests/components/cover/test_template.py +++ b/tests/components/cover/test_template.py @@ -1,17 +1,24 @@ """The tests the cover command line platform.""" - import logging import unittest -from homeassistant.core import callback from homeassistant import setup -import homeassistant.components.cover as cover -from homeassistant.const import STATE_OPEN, STATE_CLOSED +from homeassistant.core import callback +from homeassistant.components.cover import ( + ATTR_POSITION, ATTR_TILT_POSITION, DOMAIN) +from homeassistant.const import ( + ATTR_ENTITY_ID, SERVICE_CLOSE_COVER, SERVICE_CLOSE_COVER_TILT, + SERVICE_OPEN_COVER, SERVICE_OPEN_COVER_TILT, SERVICE_SET_COVER_POSITION, + SERVICE_SET_COVER_TILT_POSITION, SERVICE_STOP_COVER, + STATE_CLOSED, STATE_OPEN) from tests.common import ( get_test_home_assistant, assert_setup_component) + _LOGGER = logging.getLogger(__name__) +ENTITY_COVER = 'cover.test_template_cover' + class TestTemplateCover(unittest.TestCase): """Test the cover command line platform.""" @@ -362,7 +369,9 @@ class TestTemplateCover(unittest.TestCase): state = self.hass.states.get('cover.test_template_cover') assert state.state == STATE_CLOSED - cover.open_cover(self.hass, 'cover.test_template_cover') + self.hass.services.call( + DOMAIN, SERVICE_OPEN_COVER, + {ATTR_ENTITY_ID: ENTITY_COVER}, blocking=True) self.hass.block_till_done() assert len(self.calls) == 1 @@ -398,10 +407,14 @@ class TestTemplateCover(unittest.TestCase): state = self.hass.states.get('cover.test_template_cover') assert state.state == STATE_OPEN - cover.close_cover(self.hass, 'cover.test_template_cover') + self.hass.services.call( + DOMAIN, SERVICE_CLOSE_COVER, + {ATTR_ENTITY_ID: ENTITY_COVER}, blocking=True) self.hass.block_till_done() - cover.stop_cover(self.hass, 'cover.test_template_cover') + self.hass.services.call( + DOMAIN, SERVICE_STOP_COVER, + {ATTR_ENTITY_ID: ENTITY_COVER}, blocking=True) self.hass.block_till_done() assert len(self.calls) == 2 @@ -445,18 +458,23 @@ class TestTemplateCover(unittest.TestCase): state = self.hass.states.get('cover.test_template_cover') assert state.state == STATE_OPEN - cover.open_cover(self.hass, 'cover.test_template_cover') + self.hass.services.call( + DOMAIN, SERVICE_OPEN_COVER, + {ATTR_ENTITY_ID: ENTITY_COVER}, blocking=True) self.hass.block_till_done() state = self.hass.states.get('cover.test_template_cover') assert state.attributes.get('current_position') == 100.0 - cover.close_cover(self.hass, 'cover.test_template_cover') + self.hass.services.call( + DOMAIN, SERVICE_CLOSE_COVER, + {ATTR_ENTITY_ID: ENTITY_COVER}, blocking=True) self.hass.block_till_done() state = self.hass.states.get('cover.test_template_cover') assert state.attributes.get('current_position') == 0.0 - cover.set_cover_position(self.hass, 25, - 'cover.test_template_cover') + self.hass.services.call( + DOMAIN, SERVICE_SET_COVER_POSITION, + {ATTR_ENTITY_ID: ENTITY_COVER, ATTR_POSITION: 25}, blocking=True) self.hass.block_till_done() state = self.hass.states.get('cover.test_template_cover') assert state.attributes.get('current_position') == 25.0 @@ -490,8 +508,10 @@ class TestTemplateCover(unittest.TestCase): self.hass.start() self.hass.block_till_done() - cover.set_cover_tilt_position(self.hass, 42, - 'cover.test_template_cover') + self.hass.services.call( + DOMAIN, SERVICE_SET_COVER_TILT_POSITION, + {ATTR_ENTITY_ID: ENTITY_COVER, ATTR_TILT_POSITION: 42}, + blocking=True) self.hass.block_till_done() assert len(self.calls) == 1 @@ -525,7 +545,9 @@ class TestTemplateCover(unittest.TestCase): self.hass.start() self.hass.block_till_done() - cover.open_cover_tilt(self.hass, 'cover.test_template_cover') + self.hass.services.call( + DOMAIN, SERVICE_OPEN_COVER_TILT, + {ATTR_ENTITY_ID: ENTITY_COVER}, blocking=True) self.hass.block_till_done() assert len(self.calls) == 1 @@ -559,7 +581,9 @@ class TestTemplateCover(unittest.TestCase): self.hass.start() self.hass.block_till_done() - cover.close_cover_tilt(self.hass, 'cover.test_template_cover') + self.hass.services.call( + DOMAIN, SERVICE_CLOSE_COVER_TILT, + {ATTR_ENTITY_ID: ENTITY_COVER}, blocking=True) self.hass.block_till_done() assert len(self.calls) == 1 @@ -585,18 +609,23 @@ class TestTemplateCover(unittest.TestCase): state = self.hass.states.get('cover.test_template_cover') assert state.attributes.get('current_position') is None - cover.set_cover_position(self.hass, 42, - 'cover.test_template_cover') + self.hass.services.call( + DOMAIN, SERVICE_SET_COVER_POSITION, + {ATTR_ENTITY_ID: ENTITY_COVER, ATTR_POSITION: 42}, blocking=True) self.hass.block_till_done() state = self.hass.states.get('cover.test_template_cover') assert state.attributes.get('current_position') == 42.0 - cover.close_cover(self.hass, 'cover.test_template_cover') + self.hass.services.call( + DOMAIN, SERVICE_CLOSE_COVER, + {ATTR_ENTITY_ID: ENTITY_COVER}, blocking=True) self.hass.block_till_done() state = self.hass.states.get('cover.test_template_cover') assert state.state == STATE_CLOSED - cover.open_cover(self.hass, 'cover.test_template_cover') + self.hass.services.call( + DOMAIN, SERVICE_OPEN_COVER, + {ATTR_ENTITY_ID: ENTITY_COVER}, blocking=True) self.hass.block_till_done() state = self.hass.states.get('cover.test_template_cover') assert state.state == STATE_OPEN @@ -627,18 +656,24 @@ class TestTemplateCover(unittest.TestCase): state = self.hass.states.get('cover.test_template_cover') assert state.attributes.get('current_tilt_position') is None - cover.set_cover_tilt_position(self.hass, 42, - 'cover.test_template_cover') + self.hass.services.call( + DOMAIN, SERVICE_SET_COVER_TILT_POSITION, + {ATTR_ENTITY_ID: ENTITY_COVER, ATTR_TILT_POSITION: 42}, + blocking=True) self.hass.block_till_done() state = self.hass.states.get('cover.test_template_cover') assert state.attributes.get('current_tilt_position') == 42.0 - cover.close_cover_tilt(self.hass, 'cover.test_template_cover') + self.hass.services.call( + DOMAIN, SERVICE_CLOSE_COVER_TILT, + {ATTR_ENTITY_ID: ENTITY_COVER}, blocking=True) self.hass.block_till_done() state = self.hass.states.get('cover.test_template_cover') assert state.attributes.get('current_tilt_position') == 0.0 - cover.open_cover_tilt(self.hass, 'cover.test_template_cover') + self.hass.services.call( + DOMAIN, SERVICE_OPEN_COVER_TILT, + {ATTR_ENTITY_ID: ENTITY_COVER}, blocking=True) self.hass.block_till_done() state = self.hass.states.get('cover.test_template_cover') assert state.attributes.get('current_tilt_position') == 100.0 From b91a061cef55d38fa70a49b749c200eb793bf068 Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Tue, 25 Sep 2018 23:08:37 +0200 Subject: [PATCH 053/247] Add missing __init__ test files (#16871) --- tests/components/cover/__init__.py | 1 + tests/components/homekit/__init__.py | 1 + 2 files changed, 2 insertions(+) create mode 100644 tests/components/cover/__init__.py create mode 100644 tests/components/homekit/__init__.py diff --git a/tests/components/cover/__init__.py b/tests/components/cover/__init__.py new file mode 100644 index 00000000000..aaaf6b237cd --- /dev/null +++ b/tests/components/cover/__init__.py @@ -0,0 +1 @@ +"""Tests for the cover component.""" diff --git a/tests/components/homekit/__init__.py b/tests/components/homekit/__init__.py new file mode 100644 index 00000000000..1734367bcdb --- /dev/null +++ b/tests/components/homekit/__init__.py @@ -0,0 +1 @@ +"""Tests for the HomeKit component.""" From dc102a96a9c6edfd253b61fe8b9da89d95fb981b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Wed, 26 Sep 2018 07:49:09 +0200 Subject: [PATCH 054/247] Tibber realtime consumption, Tibber pulse (#16870) * tibber real time data * Tibber Pulse, realtime consumption * update lib --- homeassistant/components/sensor/tibber.py | 85 +++++++++++++++++++++-- requirements_all.txt | 2 +- 2 files changed, 81 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/sensor/tibber.py b/homeassistant/components/sensor/tibber.py index dbea54ff353..0f127d7dd64 100644 --- a/homeassistant/components/sensor/tibber.py +++ b/homeassistant/components/sensor/tibber.py @@ -14,14 +14,14 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_ACCESS_TOKEN +from homeassistant.const import EVENT_HOMEASSISTANT_STOP, CONF_ACCESS_TOKEN from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.entity import Entity from homeassistant.util import dt as dt_util from homeassistant.util import Throttle -REQUIREMENTS = ['pyTibber==0.5.1'] +REQUIREMENTS = ['pyTibber==0.6.0'] _LOGGER = logging.getLogger(__name__) @@ -30,6 +30,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) ICON = 'mdi:currency-usd' +ICON_RT = 'mdi:power-plug' SCAN_INTERVAL = timedelta(minutes=1) MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5) @@ -41,20 +42,26 @@ async def async_setup_platform(hass, config, async_add_entities, tibber_connection = tibber.Tibber(config[CONF_ACCESS_TOKEN], websession=async_get_clientsession(hass)) + async def _close(*_): + await tibber_connection.rt_disconnect() + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _close) + try: await tibber_connection.update_info() dev = [] for home in tibber_connection.get_homes(): await home.update_info() - dev.append(TibberSensor(home)) + dev.append(TibberSensorElPrice(home)) + if home.has_real_time_consumption: + dev.append(TibberSensorRT(home)) except (asyncio.TimeoutError, aiohttp.ClientError): raise PlatformNotReady() async_add_entities(dev, True) -class TibberSensor(Entity): - """Representation of an Tibber sensor.""" +class TibberSensorElPrice(Entity): + """Representation of an Tibber sensor for el price.""" def __init__(self, tibber_home): """Initialize the sensor.""" @@ -155,3 +162,71 @@ class TibberSensor(Entity): self._device_state_attributes['max_price'] = max_price self._device_state_attributes['min_price'] = min_price return state is not None + + +class TibberSensorRT(Entity): + """Representation of an Tibber sensor for real time consumption.""" + + def __init__(self, tibber_home): + """Initialize the sensor.""" + self._tibber_home = tibber_home + self._state = None + self._device_state_attributes = {} + self._unit_of_measurement = 'W' + nickname = tibber_home.info['viewer']['home']['appNickname'] + self._name = 'Real time consumption {}'.format(nickname) + + async def async_added_to_hass(self): + """Start unavailability tracking.""" + await self._tibber_home.rt_subscribe(self.hass.loop, + self._async_callback) + + async def _async_callback(self, payload): + """Handle received data.""" + data = payload.get('data', {}) + live_measurement = data.get('liveMeasurement', {}) + self._state = live_measurement.pop('power', None) + self._device_state_attributes = live_measurement + self.async_schedule_update_ha_state() + + @property + def device_state_attributes(self): + """Return the state attributes.""" + return self._device_state_attributes + + @property + def available(self): + """Return True if entity is available.""" + return self._tibber_home.rt_subscription_running + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def should_poll(self): + """Return the polling state.""" + return False + + @property + def state(self): + """Return the state of the device.""" + return self._state + + @property + def icon(self): + """Return the icon to use in the frontend.""" + return ICON_RT + + @property + def unit_of_measurement(self): + """Return the unit of measurement of this entity.""" + return self._unit_of_measurement + + @property + def unique_id(self): + """Return a unique ID.""" + home = self._tibber_home.info['viewer']['home'] + _id = home['meteringPointData']['consumptionEan'] + return'{}_rt_consumption'.format(_id) diff --git a/requirements_all.txt b/requirements_all.txt index b6f9dd4875e..6c8b86a03f6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -764,7 +764,7 @@ pyRFXtrx==0.23 pySwitchmate==0.4.1 # homeassistant.components.sensor.tibber -pyTibber==0.5.1 +pyTibber==0.6.0 # homeassistant.components.switch.dlink pyW215==0.6.0 From 6490ec87f3bdd5955fa9cd60acc3dc0e70646052 Mon Sep 17 00:00:00 2001 From: Gerard Date: Wed, 26 Sep 2018 08:24:59 +0200 Subject: [PATCH 055/247] Upgrade to bimmer_connected 0.5.3 (#16877) --- .../components/binary_sensor/bmw_connected_drive.py | 5 ++++- homeassistant/components/bmw_connected_drive/__init__.py | 2 +- requirements_all.txt | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/binary_sensor/bmw_connected_drive.py b/homeassistant/components/binary_sensor/bmw_connected_drive.py index 36229828d63..e3b1a941bd2 100644 --- a/homeassistant/components/binary_sensor/bmw_connected_drive.py +++ b/homeassistant/components/binary_sensor/bmw_connected_drive.py @@ -124,7 +124,10 @@ class BMWConnectedDriveSensor(BinarySensorDevice): if not check_control_messages: result['check_control_messages'] = 'OK' else: - result['check_control_messages'] = check_control_messages + cbs_list = [] + for message in check_control_messages: + cbs_list.append(message['ccmDescriptionShort']) + result['check_control_messages'] = cbs_list elif self._attribute == 'charging_status': result['charging_status'] = vehicle_state.charging_status.value # pylint: disable=protected-access diff --git a/homeassistant/components/bmw_connected_drive/__init__.py b/homeassistant/components/bmw_connected_drive/__init__.py index 12363627003..dce5961d70d 100644 --- a/homeassistant/components/bmw_connected_drive/__init__.py +++ b/homeassistant/components/bmw_connected_drive/__init__.py @@ -14,7 +14,7 @@ from homeassistant.helpers import discovery from homeassistant.helpers.event import track_utc_time_change import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['bimmer_connected==0.5.2'] +REQUIREMENTS = ['bimmer_connected==0.5.3'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 6c8b86a03f6..9f20ccb2ece 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -172,7 +172,7 @@ beautifulsoup4==4.6.3 bellows==0.7.0 # homeassistant.components.bmw_connected_drive -bimmer_connected==0.5.2 +bimmer_connected==0.5.3 # homeassistant.components.blink blinkpy==0.6.0 From 672fc61bb2b8645a32419328b07948f13afa8fed Mon Sep 17 00:00:00 2001 From: Martin Mois Date: Wed, 26 Sep 2018 08:43:09 +0200 Subject: [PATCH 056/247] aiohttp.ClientSession gets proxy information from HTTP_PROXY/HTTPS_PROXY (#16874) --- homeassistant/helpers/aiohttp_client.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/helpers/aiohttp_client.py b/homeassistant/helpers/aiohttp_client.py index 53b246c700d..01cc074aa6a 100644 --- a/homeassistant/helpers/aiohttp_client.py +++ b/homeassistant/helpers/aiohttp_client.py @@ -56,6 +56,7 @@ def async_create_clientsession(hass, verify_ssl=True, auto_cleanup=True, clientsession = aiohttp.ClientSession( loop=hass.loop, connector=connector, + trust_env=True, headers={USER_AGENT: SERVER_SOFTWARE}, **kwargs ) From e2a56721d3fe9f296286bc4338109bb25b6d1635 Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Wed, 26 Sep 2018 08:50:05 +0200 Subject: [PATCH 057/247] Remove service helper (3) (#16879) * Update duckdns * Update google_assistant * Update group * Update homematic * Update image_processing * Update input_boolean * Update input_number * Update input_select * Update input_text --- homeassistant/components/duckdns.py | 10 --- .../components/google_assistant/__init__.py | 7 -- homeassistant/components/group/__init__.py | 7 -- .../components/homematic/__init__.py | 73 ------------------- .../components/image_processing/__init__.py | 15 ---- homeassistant/components/input_boolean.py | 18 ----- homeassistant/components/input_number.py | 26 ------- homeassistant/components/input_select.py | 35 --------- homeassistant/components/input_text.py | 10 --- tests/components/group/common.py | 16 ++++ tests/components/group/test_init.py | 5 +- tests/components/image_processing/common.py | 23 ++++++ .../components/image_processing/test_init.py | 17 +++-- .../test_microsoft_face_detect.py | 3 +- .../test_microsoft_face_identify.py | 3 +- .../image_processing/test_openalpr_cloud.py | 7 +- .../image_processing/test_openalpr_local.py | 3 +- tests/components/test_duckdns.py | 18 ++++- tests/components/test_input_boolean.py | 33 ++++++++- tests/components/test_input_number.py | 41 ++++++++++- tests/components/test_input_select.py | 61 ++++++++++++++-- tests/components/test_input_text.py | 17 ++++- 22 files changed, 217 insertions(+), 231 deletions(-) create mode 100644 tests/components/group/common.py create mode 100644 tests/components/image_processing/common.py diff --git a/homeassistant/components/duckdns.py b/homeassistant/components/duckdns.py index 178e1579538..6ec2e6b1e03 100644 --- a/homeassistant/components/duckdns.py +++ b/homeassistant/components/duckdns.py @@ -11,7 +11,6 @@ import logging import voluptuous as vol from homeassistant.const import CONF_ACCESS_TOKEN, CONF_DOMAIN -from homeassistant.loader import bind_hass import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -40,15 +39,6 @@ SERVICE_TXT_SCHEMA = vol.Schema({ }) -@bind_hass -@asyncio.coroutine -def async_set_txt(hass, txt): - """Set the txt record. Pass in None to remove it.""" - yield from hass.services.async_call(DOMAIN, SERVICE_SET_TXT, { - ATTR_TXT: txt - }, blocking=True) - - @asyncio.coroutine def async_setup(hass, config): """Initialize the DuckDNS component.""" diff --git a/homeassistant/components/google_assistant/__init__.py b/homeassistant/components/google_assistant/__init__.py index 22569af1f86..b0b580dc6ac 100644 --- a/homeassistant/components/google_assistant/__init__.py +++ b/homeassistant/components/google_assistant/__init__.py @@ -19,7 +19,6 @@ from homeassistant.core import HomeAssistant from homeassistant.const import CONF_NAME from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.loader import bind_hass from .const import ( DOMAIN, CONF_PROJECT_ID, CONF_CLIENT_ID, CONF_ACCESS_TOKEN, @@ -63,12 +62,6 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA) -@bind_hass -def request_sync(hass): - """Request sync.""" - hass.services.call(DOMAIN, SERVICE_REQUEST_SYNC) - - async def async_setup(hass: HomeAssistant, yaml_config: Dict[str, Any]): """Activate Google Actions component.""" config = yaml_config.get(DOMAIN, {}) diff --git a/homeassistant/components/group/__init__.py b/homeassistant/components/group/__init__.py index eda65d1895d..7b5a75dc13f 100644 --- a/homeassistant/components/group/__init__.py +++ b/homeassistant/components/group/__init__.py @@ -133,13 +133,6 @@ def async_reload(hass): hass.async_add_job(hass.services.async_call(DOMAIN, SERVICE_RELOAD)) -@bind_hass -def set_visibility(hass, entity_id=None, visible=True): - """Hide or shows a group.""" - data = {ATTR_ENTITY_ID: entity_id, ATTR_VISIBLE: visible} - hass.services.call(DOMAIN, SERVICE_SET_VISIBILITY, data) - - @bind_hass def set_group(hass, object_id, name=None, entity_ids=None, visible=None, icon=None, view=None, control=None, add=None): diff --git a/homeassistant/components/homematic/__init__.py b/homeassistant/components/homematic/__init__.py index 4e6b3f04ee1..3d6eb69bb5e 100644 --- a/homeassistant/components/homematic/__init__.py +++ b/homeassistant/components/homematic/__init__.py @@ -18,7 +18,6 @@ from homeassistant.const import ( from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity -from homeassistant.loader import bind_hass REQUIREMENTS = ['pyhomematic==0.1.49'] @@ -245,78 +244,6 @@ SCHEMA_SERVICE_PUT_PARAMSET = vol.Schema({ }) -@bind_hass -def virtualkey(hass, address, channel, param, interface=None): - """Send virtual keypress to homematic controller.""" - data = { - ATTR_ADDRESS: address, - ATTR_CHANNEL: channel, - ATTR_PARAM: param, - ATTR_INTERFACE: interface, - } - - hass.services.call(DOMAIN, SERVICE_VIRTUALKEY, data) - - -@bind_hass -def set_variable_value(hass, entity_id, value): - """Change value of a Homematic system variable.""" - data = { - ATTR_ENTITY_ID: entity_id, - ATTR_VALUE: value, - } - - hass.services.call(DOMAIN, SERVICE_SET_VARIABLE_VALUE, data) - - -@bind_hass -def set_device_value(hass, address, channel, param, value, interface=None): - """Call setValue XML-RPC method of supplied interface.""" - data = { - ATTR_ADDRESS: address, - ATTR_CHANNEL: channel, - ATTR_PARAM: param, - ATTR_VALUE: value, - ATTR_INTERFACE: interface, - } - - hass.services.call(DOMAIN, SERVICE_SET_DEVICE_VALUE, data) - - -@bind_hass -def put_paramset(hass, interface, address, paramset_key, paramset): - """Call putParamset XML-RPC method of supplied interface.""" - data = { - ATTR_INTERFACE: interface, - ATTR_ADDRESS: address, - ATTR_PARAMSET_KEY: paramset_key, - ATTR_PARAMSET: paramset, - } - - hass.services.call(DOMAIN, SERVICE_PUT_PARAMSET, data) - - -@bind_hass -def set_install_mode(hass, interface, mode=None, time=None, address=None): - """Call setInstallMode XML-RPC method of supplied interface.""" - data = { - key: value for key, value in ( - (ATTR_INTERFACE, interface), - (ATTR_MODE, mode), - (ATTR_TIME, time), - (ATTR_ADDRESS, address) - ) if value - } - - hass.services.call(DOMAIN, SERVICE_SET_INSTALL_MODE, data) - - -@bind_hass -def reconnect(hass): - """Reconnect to CCU/Homegear.""" - hass.services.call(DOMAIN, SERVICE_RECONNECT, {}) - - def setup(hass, config): """Set up the Homematic component.""" from pyhomematic import HMConnection diff --git a/homeassistant/components/image_processing/__init__.py b/homeassistant/components/image_processing/__init__.py index 480ec31da7d..84d92361541 100644 --- a/homeassistant/components/image_processing/__init__.py +++ b/homeassistant/components/image_processing/__init__.py @@ -17,7 +17,6 @@ from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent -from homeassistant.loader import bind_hass from homeassistant.util.async_ import run_callback_threadsafe _LOGGER = logging.getLogger(__name__) @@ -67,20 +66,6 @@ SERVICE_SCAN_SCHEMA = vol.Schema({ }) -@bind_hass -def scan(hass, entity_id=None): - """Force process of all cameras or given entity.""" - hass.add_job(async_scan, hass, entity_id) - - -@callback -@bind_hass -def async_scan(hass, entity_id=None): - """Force process of all cameras or given entity.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else None - hass.async_add_job(hass.services.async_call(DOMAIN, SERVICE_SCAN, data)) - - async def async_setup(hass, config): """Set up the image processing.""" component = EntityComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL) diff --git a/homeassistant/components/input_boolean.py b/homeassistant/components/input_boolean.py index b9c4dcc685e..18c9808c6d2 100644 --- a/homeassistant/components/input_boolean.py +++ b/homeassistant/components/input_boolean.py @@ -46,24 +46,6 @@ def is_on(hass, entity_id): return hass.states.is_state(entity_id, STATE_ON) -@bind_hass -def turn_on(hass, entity_id): - """Set input_boolean to True.""" - hass.services.call(DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: entity_id}) - - -@bind_hass -def turn_off(hass, entity_id): - """Set input_boolean to False.""" - hass.services.call(DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: entity_id}) - - -@bind_hass -def toggle(hass, entity_id): - """Set input_boolean to False.""" - hass.services.call(DOMAIN, SERVICE_TOGGLE, {ATTR_ENTITY_ID: entity_id}) - - async def async_setup(hass, config): """Set up an input boolean.""" component = EntityComponent(_LOGGER, DOMAIN, hass) diff --git a/homeassistant/components/input_number.py b/homeassistant/components/input_number.py index 2f25ca143b8..2eb811e1fb0 100644 --- a/homeassistant/components/input_number.py +++ b/homeassistant/components/input_number.py @@ -15,7 +15,6 @@ from homeassistant.const import ( from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.restore_state import async_get_last_state -from homeassistant.loader import bind_hass _LOGGER = logging.getLogger(__name__) @@ -82,31 +81,6 @@ CONFIG_SCHEMA = vol.Schema({ }, required=True, extra=vol.ALLOW_EXTRA) -@bind_hass -def set_value(hass, entity_id, value): - """Set input_number to value.""" - hass.services.call(DOMAIN, SERVICE_SET_VALUE, { - ATTR_ENTITY_ID: entity_id, - ATTR_VALUE: value, - }) - - -@bind_hass -def increment(hass, entity_id): - """Increment value of entity.""" - hass.services.call(DOMAIN, SERVICE_INCREMENT, { - ATTR_ENTITY_ID: entity_id - }) - - -@bind_hass -def decrement(hass, entity_id): - """Decrement value of entity.""" - hass.services.call(DOMAIN, SERVICE_DECREMENT, { - ATTR_ENTITY_ID: entity_id - }) - - @asyncio.coroutine def async_setup(hass, config): """Set up an input slider.""" diff --git a/homeassistant/components/input_select.py b/homeassistant/components/input_select.py index 04e9b04787c..51175efecbd 100644 --- a/homeassistant/components/input_select.py +++ b/homeassistant/components/input_select.py @@ -10,7 +10,6 @@ import logging import voluptuous as vol from homeassistant.const import ATTR_ENTITY_ID, CONF_ICON, CONF_NAME -from homeassistant.loader import bind_hass import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent @@ -78,40 +77,6 @@ CONFIG_SCHEMA = vol.Schema({ }, required=True, extra=vol.ALLOW_EXTRA) -@bind_hass -def select_option(hass, entity_id, option): - """Set value of input_select.""" - hass.services.call(DOMAIN, SERVICE_SELECT_OPTION, { - ATTR_ENTITY_ID: entity_id, - ATTR_OPTION: option, - }) - - -@bind_hass -def select_next(hass, entity_id): - """Set next value of input_select.""" - hass.services.call(DOMAIN, SERVICE_SELECT_NEXT, { - ATTR_ENTITY_ID: entity_id, - }) - - -@bind_hass -def select_previous(hass, entity_id): - """Set previous value of input_select.""" - hass.services.call(DOMAIN, SERVICE_SELECT_PREVIOUS, { - ATTR_ENTITY_ID: entity_id, - }) - - -@bind_hass -def set_options(hass, entity_id, options): - """Set options of input_select.""" - hass.services.call(DOMAIN, SERVICE_SET_OPTIONS, { - ATTR_ENTITY_ID: entity_id, - ATTR_OPTIONS: options, - }) - - @asyncio.coroutine def async_setup(hass, config): """Set up an input select.""" diff --git a/homeassistant/components/input_text.py b/homeassistant/components/input_text.py index 2cb4f58a130..fcc2352f523 100644 --- a/homeassistant/components/input_text.py +++ b/homeassistant/components/input_text.py @@ -12,7 +12,6 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_UNIT_OF_MEASUREMENT, CONF_ICON, CONF_NAME, CONF_MODE) -from homeassistant.loader import bind_hass from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.restore_state import async_get_last_state @@ -74,15 +73,6 @@ CONFIG_SCHEMA = vol.Schema({ }, required=True, extra=vol.ALLOW_EXTRA) -@bind_hass -def set_value(hass, entity_id, value): - """Set input_text to value.""" - hass.services.call(DOMAIN, SERVICE_SET_VALUE, { - ATTR_ENTITY_ID: entity_id, - ATTR_VALUE: value, - }) - - @asyncio.coroutine def async_setup(hass, config): """Set up an input text box.""" diff --git a/tests/components/group/common.py b/tests/components/group/common.py new file mode 100644 index 00000000000..1a59eb265e0 --- /dev/null +++ b/tests/components/group/common.py @@ -0,0 +1,16 @@ +"""Collection of helper methods. + +All containing methods are legacy helpers that should not be used by new +components. Instead call the service directly. +""" +from homeassistant.components.group import ATTR_VISIBLE, DOMAIN, \ + SERVICE_SET_VISIBILITY +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.loader import bind_hass + + +@bind_hass +def set_visibility(hass, entity_id=None, visible=True): + """Hide or shows a group.""" + data = {ATTR_ENTITY_ID: entity_id, ATTR_VISIBLE: visible} + hass.services.call(DOMAIN, SERVICE_SET_VISIBILITY, data) diff --git a/tests/components/group/test_init.py b/tests/components/group/test_init.py index 47101dd415a..619ce86583a 100644 --- a/tests/components/group/test_init.py +++ b/tests/components/group/test_init.py @@ -12,6 +12,7 @@ from homeassistant.const import ( import homeassistant.components.group as group from tests.common import get_test_home_assistant, assert_setup_component +from tests.components.group import common class TestComponentsGroup(unittest.TestCase): @@ -385,13 +386,13 @@ class TestComponentsGroup(unittest.TestCase): group_entity_id = group.ENTITY_ID_FORMAT.format('test_group') # Hide the group - group.set_visibility(self.hass, group_entity_id, False) + common.set_visibility(self.hass, group_entity_id, False) self.hass.block_till_done() group_state = self.hass.states.get(group_entity_id) self.assertTrue(group_state.attributes.get(ATTR_HIDDEN)) # Show it again - group.set_visibility(self.hass, group_entity_id, True) + common.set_visibility(self.hass, group_entity_id, True) self.hass.block_till_done() group_state = self.hass.states.get(group_entity_id) self.assertIsNone(group_state.attributes.get(ATTR_HIDDEN)) diff --git a/tests/components/image_processing/common.py b/tests/components/image_processing/common.py new file mode 100644 index 00000000000..b767884503d --- /dev/null +++ b/tests/components/image_processing/common.py @@ -0,0 +1,23 @@ +"""Collection of helper methods. + +All containing methods are legacy helpers that should not be used by new +components. Instead call the service directly. +""" +from homeassistant.components.image_processing import DOMAIN, SERVICE_SCAN +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.core import callback +from homeassistant.loader import bind_hass + + +@bind_hass +def scan(hass, entity_id=None): + """Force process of all cameras or given entity.""" + hass.add_job(async_scan, hass, entity_id) + + +@callback +@bind_hass +def async_scan(hass, entity_id=None): + """Force process of all cameras or given entity.""" + data = {ATTR_ENTITY_ID: entity_id} if entity_id else None + hass.async_add_job(hass.services.async_call(DOMAIN, SERVICE_SCAN, data)) diff --git a/tests/components/image_processing/test_init.py b/tests/components/image_processing/test_init.py index 4240e173b26..7a31b2ffadf 100644 --- a/tests/components/image_processing/test_init.py +++ b/tests/components/image_processing/test_init.py @@ -10,6 +10,7 @@ import homeassistant.components.image_processing as ip from tests.common import ( get_test_home_assistant, get_test_instance_port, assert_setup_component) +from tests.components.image_processing import common class TestSetupImageProcessing: @@ -85,7 +86,7 @@ class TestImageProcessing: """Grab an image from camera entity.""" self.hass.start() - ip.scan(self.hass, entity_id='image_processing.test') + common.scan(self.hass, entity_id='image_processing.test') self.hass.block_till_done() state = self.hass.states.get('image_processing.test') @@ -100,7 +101,7 @@ class TestImageProcessing: """Try to get image without exists camera.""" self.hass.states.remove('camera.demo_camera') - ip.scan(self.hass, entity_id='image_processing.test') + common.scan(self.hass, entity_id='image_processing.test') self.hass.block_till_done() state = self.hass.states.get('image_processing.test') @@ -152,7 +153,7 @@ class TestImageProcessingAlpr: """Set up and scan a picture and test plates from event.""" aioclient_mock.get(self.url, content=b'image') - ip.scan(self.hass, entity_id='image_processing.demo_alpr') + common.scan(self.hass, entity_id='image_processing.demo_alpr') self.hass.block_till_done() state = self.hass.states.get('image_processing.demo_alpr') @@ -171,8 +172,8 @@ class TestImageProcessingAlpr: """Set up and scan a picture and test plates from event.""" aioclient_mock.get(self.url, content=b'image') - ip.scan(self.hass, entity_id='image_processing.demo_alpr') - ip.scan(self.hass, entity_id='image_processing.demo_alpr') + common.scan(self.hass, entity_id='image_processing.demo_alpr') + common.scan(self.hass, entity_id='image_processing.demo_alpr') self.hass.block_till_done() state = self.hass.states.get('image_processing.demo_alpr') @@ -195,7 +196,7 @@ class TestImageProcessingAlpr: """Set up and scan a picture and test plates from event.""" aioclient_mock.get(self.url, content=b'image') - ip.scan(self.hass, entity_id='image_processing.demo_alpr') + common.scan(self.hass, entity_id='image_processing.demo_alpr') self.hass.block_till_done() state = self.hass.states.get('image_processing.demo_alpr') @@ -254,7 +255,7 @@ class TestImageProcessingFace: """Set up and scan a picture and test faces from event.""" aioclient_mock.get(self.url, content=b'image') - ip.scan(self.hass, entity_id='image_processing.demo_face') + common.scan(self.hass, entity_id='image_processing.demo_face') self.hass.block_till_done() state = self.hass.states.get('image_processing.demo_face') @@ -279,7 +280,7 @@ class TestImageProcessingFace: """Set up and scan a picture and test faces from event.""" aioclient_mock.get(self.url, content=b'image') - ip.scan(self.hass, entity_id='image_processing.demo_face') + common.scan(self.hass, entity_id='image_processing.demo_face') self.hass.block_till_done() state = self.hass.states.get('image_processing.demo_face') diff --git a/tests/components/image_processing/test_microsoft_face_detect.py b/tests/components/image_processing/test_microsoft_face_detect.py index 9047c5b8475..c7528c346ee 100644 --- a/tests/components/image_processing/test_microsoft_face_detect.py +++ b/tests/components/image_processing/test_microsoft_face_detect.py @@ -9,6 +9,7 @@ import homeassistant.components.microsoft_face as mf from tests.common import ( get_test_home_assistant, assert_setup_component, load_fixture, mock_coro) +from tests.components.image_processing import common class TestMicrosoftFaceDetectSetup: @@ -146,7 +147,7 @@ class TestMicrosoftFaceDetect: params={'returnFaceAttributes': "age,gender"} ) - ip.scan(self.hass, entity_id='image_processing.test_local') + common.scan(self.hass, entity_id='image_processing.test_local') self.hass.block_till_done() state = self.hass.states.get('image_processing.test_local') diff --git a/tests/components/image_processing/test_microsoft_face_identify.py b/tests/components/image_processing/test_microsoft_face_identify.py index 6d3eae38728..892326e5bff 100644 --- a/tests/components/image_processing/test_microsoft_face_identify.py +++ b/tests/components/image_processing/test_microsoft_face_identify.py @@ -9,6 +9,7 @@ import homeassistant.components.microsoft_face as mf from tests.common import ( get_test_home_assistant, assert_setup_component, load_fixture, mock_coro) +from tests.components.image_processing import common class TestMicrosoftFaceIdentifySetup: @@ -150,7 +151,7 @@ class TestMicrosoftFaceIdentify: text=load_fixture('microsoft_face_identify.json') ) - ip.scan(self.hass, entity_id='image_processing.test_local') + common.scan(self.hass, entity_id='image_processing.test_local') self.hass.block_till_done() state = self.hass.states.get('image_processing.test_local') diff --git a/tests/components/image_processing/test_openalpr_cloud.py b/tests/components/image_processing/test_openalpr_cloud.py index 2d6015e3fe7..8a71db7fb7b 100644 --- a/tests/components/image_processing/test_openalpr_cloud.py +++ b/tests/components/image_processing/test_openalpr_cloud.py @@ -10,6 +10,7 @@ from homeassistant.components.image_processing.openalpr_cloud import ( from tests.common import ( get_test_home_assistant, assert_setup_component, load_fixture, mock_coro) +from tests.components.image_processing import common class TestOpenAlprCloudSetup: @@ -160,7 +161,7 @@ class TestOpenAlprCloud: with patch('homeassistant.components.camera.async_get_image', return_value=mock_coro( camera.Image('image/jpeg', b'image'))): - ip.scan(self.hass, entity_id='image_processing.test_local') + common.scan(self.hass, entity_id='image_processing.test_local') self.hass.block_till_done() state = self.hass.states.get('image_processing.test_local') @@ -188,7 +189,7 @@ class TestOpenAlprCloud: with patch('homeassistant.components.camera.async_get_image', return_value=mock_coro( camera.Image('image/jpeg', b'image'))): - ip.scan(self.hass, entity_id='image_processing.test_local') + common.scan(self.hass, entity_id='image_processing.test_local') self.hass.block_till_done() assert len(aioclient_mock.mock_calls) == 1 @@ -204,7 +205,7 @@ class TestOpenAlprCloud: with patch('homeassistant.components.camera.async_get_image', return_value=mock_coro( camera.Image('image/jpeg', b'image'))): - ip.scan(self.hass, entity_id='image_processing.test_local') + common.scan(self.hass, entity_id='image_processing.test_local') self.hass.block_till_done() assert len(aioclient_mock.mock_calls) == 1 diff --git a/tests/components/image_processing/test_openalpr_local.py b/tests/components/image_processing/test_openalpr_local.py index 772d66670a0..6d860da3313 100644 --- a/tests/components/image_processing/test_openalpr_local.py +++ b/tests/components/image_processing/test_openalpr_local.py @@ -9,6 +9,7 @@ import homeassistant.components.image_processing as ip from tests.common import ( get_test_home_assistant, assert_setup_component, load_fixture) +from tests.components.image_processing import common @asyncio.coroutine @@ -146,7 +147,7 @@ class TestOpenAlprLocal: """Set up and scan a picture and test plates from event.""" aioclient_mock.get(self.url, content=b'image') - ip.scan(self.hass, entity_id='image_processing.test_local') + common.scan(self.hass, entity_id='image_processing.test_local') self.hass.block_till_done() state = self.hass.states.get('image_processing.test_local') diff --git a/tests/components/test_duckdns.py b/tests/components/test_duckdns.py index d64ffbca81f..c3ece8a70fd 100644 --- a/tests/components/test_duckdns.py +++ b/tests/components/test_duckdns.py @@ -4,6 +4,7 @@ from datetime import timedelta import pytest +from homeassistant.loader import bind_hass from homeassistant.setup import async_setup_component from homeassistant.components import duckdns from homeassistant.util.dt import utcnow @@ -14,6 +15,19 @@ DOMAIN = 'bla' TOKEN = 'abcdefgh' +@bind_hass +@asyncio.coroutine +def async_set_txt(hass, txt): + """Set the txt record. Pass in None to remove it. + + This is a legacy helper method. Do not use it for new tests. + """ + yield from hass.services.async_call( + duckdns.DOMAIN, duckdns.SERVICE_SET_TXT, { + duckdns.ATTR_TXT: txt + }, blocking=True) + + @pytest.fixture def setup_duckdns(hass, aioclient_mock): """Fixture that sets up DuckDNS.""" @@ -84,7 +98,7 @@ def test_service_set_txt(hass, aioclient_mock, setup_duckdns): }, text='OK') assert aioclient_mock.call_count == 0 - yield from hass.components.duckdns.async_set_txt('some-txt') + yield from async_set_txt(hass, 'some-txt') assert aioclient_mock.call_count == 1 @@ -102,5 +116,5 @@ def test_service_clear_txt(hass, aioclient_mock, setup_duckdns): }, text='OK') assert aioclient_mock.call_count == 0 - yield from hass.components.duckdns.async_set_txt(None) + yield from async_set_txt(hass, None) assert aioclient_mock.call_count == 1 diff --git a/tests/components/test_input_boolean.py b/tests/components/test_input_boolean.py index 999e7ac100f..b947155e6b2 100644 --- a/tests/components/test_input_boolean.py +++ b/tests/components/test_input_boolean.py @@ -7,9 +7,11 @@ import logging from homeassistant.core import CoreState, State, Context from homeassistant.setup import setup_component, async_setup_component from homeassistant.components.input_boolean import ( - DOMAIN, is_on, toggle, turn_off, turn_on, CONF_INITIAL) + is_on, CONF_INITIAL, DOMAIN) from homeassistant.const import ( - STATE_ON, STATE_OFF, ATTR_ICON, ATTR_FRIENDLY_NAME) + STATE_ON, STATE_OFF, ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, ATTR_ICON, + SERVICE_TOGGLE, SERVICE_TURN_OFF, SERVICE_TURN_ON) +from homeassistant.loader import bind_hass from tests.common import ( get_test_home_assistant, mock_component, mock_restore_cache) @@ -17,6 +19,33 @@ from tests.common import ( _LOGGER = logging.getLogger(__name__) +@bind_hass +def toggle(hass, entity_id): + """Set input_boolean to False. + + This is a legacy helper method. Do not use it for new tests. + """ + hass.services.call(DOMAIN, SERVICE_TOGGLE, {ATTR_ENTITY_ID: entity_id}) + + +@bind_hass +def turn_on(hass, entity_id): + """Set input_boolean to True. + + This is a legacy helper method. Do not use it for new tests. + """ + hass.services.call(DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: entity_id}) + + +@bind_hass +def turn_off(hass, entity_id): + """Set input_boolean to False. + + This is a legacy helper method. Do not use it for new tests. + """ + hass.services.call(DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: entity_id}) + + class TestInputBoolean(unittest.TestCase): """Test the input boolean module.""" diff --git a/tests/components/test_input_number.py b/tests/components/test_input_number.py index 659aaa524d9..a53704e1d10 100644 --- a/tests/components/test_input_number.py +++ b/tests/components/test_input_number.py @@ -4,13 +4,50 @@ import asyncio import unittest from homeassistant.core import CoreState, State, Context -from homeassistant.setup import setup_component, async_setup_component from homeassistant.components.input_number import ( - DOMAIN, set_value, increment, decrement) + ATTR_VALUE, DOMAIN, SERVICE_DECREMENT, SERVICE_INCREMENT, + SERVICE_SET_VALUE) +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.loader import bind_hass +from homeassistant.setup import setup_component, async_setup_component from tests.common import get_test_home_assistant, mock_restore_cache +@bind_hass +def set_value(hass, entity_id, value): + """Set input_number to value. + + This is a legacy helper method. Do not use it for new tests. + """ + hass.services.call(DOMAIN, SERVICE_SET_VALUE, { + ATTR_ENTITY_ID: entity_id, + ATTR_VALUE: value, + }) + + +@bind_hass +def increment(hass, entity_id): + """Increment value of entity. + + This is a legacy helper method. Do not use it for new tests. + """ + hass.services.call(DOMAIN, SERVICE_INCREMENT, { + ATTR_ENTITY_ID: entity_id + }) + + +@bind_hass +def decrement(hass, entity_id): + """Decrement value of entity. + + This is a legacy helper method. Do not use it for new tests. + """ + hass.services.call(DOMAIN, SERVICE_DECREMENT, { + ATTR_ENTITY_ID: entity_id + }) + + class TestInputNumber(unittest.TestCase): """Test the input number component.""" diff --git a/tests/components/test_input_select.py b/tests/components/test_input_select.py index 1c73abfbb94..938c93fa099 100644 --- a/tests/components/test_input_select.py +++ b/tests/components/test_input_select.py @@ -3,15 +3,62 @@ import asyncio import unittest -from tests.common import get_test_home_assistant, mock_restore_cache - +from homeassistant.loader import bind_hass +from homeassistant.components.input_select import ( + ATTR_OPTION, ATTR_OPTIONS, DOMAIN, SERVICE_SET_OPTIONS, + SERVICE_SELECT_NEXT, SERVICE_SELECT_OPTION, SERVICE_SELECT_PREVIOUS) +from homeassistant.const import ( + ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, ATTR_ICON) from homeassistant.core import State, Context from homeassistant.setup import setup_component, async_setup_component -from homeassistant.components.input_select import ( - ATTR_OPTIONS, DOMAIN, SERVICE_SET_OPTIONS, - select_option, select_next, select_previous) -from homeassistant.const import ( - ATTR_ICON, ATTR_FRIENDLY_NAME) + +from tests.common import get_test_home_assistant, mock_restore_cache + + +@bind_hass +def select_option(hass, entity_id, option): + """Set value of input_select. + + This is a legacy helper method. Do not use it for new tests. + """ + hass.services.call(DOMAIN, SERVICE_SELECT_OPTION, { + ATTR_ENTITY_ID: entity_id, + ATTR_OPTION: option, + }) + + +@bind_hass +def select_next(hass, entity_id): + """Set next value of input_select. + + This is a legacy helper method. Do not use it for new tests. + """ + hass.services.call(DOMAIN, SERVICE_SELECT_NEXT, { + ATTR_ENTITY_ID: entity_id, + }) + + +@bind_hass +def select_previous(hass, entity_id): + """Set previous value of input_select. + + This is a legacy helper method. Do not use it for new tests. + """ + hass.services.call(DOMAIN, SERVICE_SELECT_PREVIOUS, { + ATTR_ENTITY_ID: entity_id, + }) + + +@bind_hass +def set_options(hass, entity_id, options): + """Set options of input_select. + + This is a legacy helper method. Do not use it for new tests. + """ + hass.services.call(DOMAIN, SERVICE_SET_OPTIONS, { + ATTR_ENTITY_ID: entity_id, + ATTR_OPTIONS: options, + }) class TestInputSelect(unittest.TestCase): diff --git a/tests/components/test_input_text.py b/tests/components/test_input_text.py index 7c8a0e65023..bea145390eb 100644 --- a/tests/components/test_input_text.py +++ b/tests/components/test_input_text.py @@ -3,13 +3,28 @@ import asyncio import unittest +from homeassistant.components.input_text import ( + ATTR_VALUE, DOMAIN, SERVICE_SET_VALUE) +from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import CoreState, State, Context +from homeassistant.loader import bind_hass from homeassistant.setup import setup_component, async_setup_component -from homeassistant.components.input_text import (DOMAIN, set_value) from tests.common import get_test_home_assistant, mock_restore_cache +@bind_hass +def set_value(hass, entity_id, value): + """Set input_text to value. + + This is a legacy helper method. Do not use it for new tests. + """ + hass.services.call(DOMAIN, SERVICE_SET_VALUE, { + ATTR_ENTITY_ID: entity_id, + ATTR_VALUE: value, + }) + + class TestInputText(unittest.TestCase): """Test the input slider component.""" From ba2b8512c5e475d8536c4a64b2e67b09f1a89e62 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 26 Sep 2018 08:52:22 +0200 Subject: [PATCH 058/247] Make ring sync again (#16866) --- homeassistant/components/camera/ring.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/camera/ring.py b/homeassistant/components/camera/ring.py index f629b501819..d0cb6443fc7 100644 --- a/homeassistant/components/camera/ring.py +++ b/homeassistant/components/camera/ring.py @@ -39,9 +39,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +def setup_platform(hass, config, add_entities, discovery_info=None): """Set up a Ring Door Bell and StickUp Camera.""" ring = hass.data[DATA_RING] @@ -67,14 +65,14 @@ def async_setup_platform(hass, config, async_add_entities, ''' following cameras: {}.'''.format(cameras) _LOGGER.error(err_msg) - hass.components.persistent_notification.async_create( + hass.components.persistent_notification.create( 'Error: {}
' 'You will need to restart hass after fixing.' ''.format(err_msg), title=NOTIFICATION_TITLE, notification_id=NOTIFICATION_ID) - async_add_entities(cams, True) + add_entities(cams, True) return True From bfa1c55803507f6632fd8907de9336dd27062afd Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Wed, 26 Sep 2018 08:53:24 +0200 Subject: [PATCH 059/247] Fix fan_init test (#16865) * Fix fan_init test * Readd __init__.py --- tests/components/fan/__init__.py | 38 ----------------------------- tests/components/fan/test_init.py | 40 +++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 38 deletions(-) create mode 100644 tests/components/fan/test_init.py diff --git a/tests/components/fan/__init__.py b/tests/components/fan/__init__.py index 28ae7f4e249..e7cc83217ef 100644 --- a/tests/components/fan/__init__.py +++ b/tests/components/fan/__init__.py @@ -1,39 +1 @@ """Tests for fan platforms.""" - -import unittest - -from homeassistant.components.fan import FanEntity - - -class BaseFan(FanEntity): - """Implementation of the abstract FanEntity.""" - - def __init__(self): - """Initialize the fan.""" - pass - - -class TestFanEntity(unittest.TestCase): - """Test coverage for base fan entity class.""" - - def setUp(self): - """Set up test data.""" - self.fan = BaseFan() - - def tearDown(self): - """Tear down unit test data.""" - self.fan = None - - def test_fanentity(self): - """Test fan entity methods.""" - self.assertIsNone(self.fan.state) - self.assertEqual(0, len(self.fan.speed_list)) - self.assertEqual(0, self.fan.supported_features) - self.assertEqual({}, self.fan.state_attributes) - # Test set_speed not required - self.fan.set_speed() - self.fan.oscillate() - with self.assertRaises(NotImplementedError): - self.fan.turn_on() - with self.assertRaises(NotImplementedError): - self.fan.turn_off() diff --git a/tests/components/fan/test_init.py b/tests/components/fan/test_init.py new file mode 100644 index 00000000000..15f9a79d2d2 --- /dev/null +++ b/tests/components/fan/test_init.py @@ -0,0 +1,40 @@ +"""Tests for fan platforms.""" + +import unittest + +from homeassistant.components.fan import FanEntity + + +class BaseFan(FanEntity): + """Implementation of the abstract FanEntity.""" + + def __init__(self): + """Initialize the fan.""" + pass + + +class TestFanEntity(unittest.TestCase): + """Test coverage for base fan entity class.""" + + def setUp(self): + """Set up test data.""" + self.fan = BaseFan() + + def tearDown(self): + """Tear down unit test data.""" + self.fan = None + + def test_fanentity(self): + """Test fan entity methods.""" + self.assertEqual('on', self.fan.state) + self.assertEqual(0, len(self.fan.speed_list)) + self.assertEqual(0, self.fan.supported_features) + self.assertEqual({'speed_list': []}, self.fan.state_attributes) + # Test set_speed not required + self.fan.oscillate(True) + with self.assertRaises(NotImplementedError): + self.fan.set_speed('slow') + with self.assertRaises(NotImplementedError): + self.fan.turn_on() + with self.assertRaises(NotImplementedError): + self.fan.turn_off() From 3cba2e695cb7e23cc5afd70f2ba2d132a4ae68f9 Mon Sep 17 00:00:00 2001 From: Charles Garwood Date: Wed, 26 Sep 2018 02:56:23 -0400 Subject: [PATCH 060/247] Device Registry Support for iOS Sensors (#16862) * Add device_info property to iOS sensors for device registry * Remove unused logger import * Fix spacing * lint * Lint --- homeassistant/components/sensor/ios.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/homeassistant/components/sensor/ios.py b/homeassistant/components/sensor/ios.py index a50d1161676..d206cd1df87 100644 --- a/homeassistant/components/sensor/ios.py +++ b/homeassistant/components/sensor/ios.py @@ -46,6 +46,21 @@ class IOSSensor(Entity): self._state = None self._unit_of_measurement = SENSOR_TYPES[sensor_type][1] + @property + def device_info(self): + """Return information about the device.""" + return { + 'identifiers': { + (ios.DOMAIN, + self._device[ios.ATTR_DEVICE][ios.ATTR_DEVICE_PERMANENT_ID]), + }, + 'name': self._device[ios.ATTR_DEVICE][ios.ATTR_DEVICE_NAME], + 'manufacturer': 'Apple', + 'model': self._device[ios.ATTR_DEVICE][ios.ATTR_DEVICE_TYPE], + 'sw_version': + self._device[ios.ATTR_DEVICE][ios.ATTR_DEVICE_SYSTEM_VERSION], + } + @property def name(self): """Return the name of the iOS sensor.""" From 92a50689775eeb04af7dbcd91578d8718b9eb221 Mon Sep 17 00:00:00 2001 From: Jason Hu Date: Tue, 25 Sep 2018 23:57:55 -0700 Subject: [PATCH 061/247] Use HA native OAuth2 flow for google assistant components (#16848) * Use HA native OAuth2 flow for google assistant components * Lint * Force breaking changes * Fix CONFIG_SCHEMA --- .../components/google_assistant/__init__.py | 44 ++++------ .../components/google_assistant/auth.py | 83 ------------------- .../components/google_assistant/const.py | 3 - .../components/google_assistant/http.py | 25 ++---- .../google_assistant/test_google_assistant.py | 46 ++++------ .../components/google_assistant/test_init.py | 4 - 6 files changed, 43 insertions(+), 162 deletions(-) delete mode 100644 homeassistant/components/google_assistant/auth.py diff --git a/homeassistant/components/google_assistant/__init__.py b/homeassistant/components/google_assistant/__init__.py index b0b580dc6ac..8d4ac9f01c9 100644 --- a/homeassistant/components/google_assistant/__init__.py +++ b/homeassistant/components/google_assistant/__init__.py @@ -14,20 +14,18 @@ import async_timeout import voluptuous as vol # Typing imports -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.const import CONF_NAME from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import ( - DOMAIN, CONF_PROJECT_ID, CONF_CLIENT_ID, CONF_ACCESS_TOKEN, - CONF_EXPOSE_BY_DEFAULT, DEFAULT_EXPOSE_BY_DEFAULT, CONF_EXPOSED_DOMAINS, - DEFAULT_EXPOSED_DOMAINS, CONF_AGENT_USER_ID, CONF_API_KEY, + DOMAIN, CONF_PROJECT_ID, CONF_EXPOSE_BY_DEFAULT, DEFAULT_EXPOSE_BY_DEFAULT, + CONF_EXPOSED_DOMAINS, DEFAULT_EXPOSED_DOMAINS, CONF_API_KEY, SERVICE_REQUEST_SYNC, REQUEST_SYNC_BASE_URL, CONF_ENTITY_CONFIG, CONF_EXPOSE, CONF_ALIASES, CONF_ROOM_HINT ) -from .auth import GoogleAssistantAuthView from .http import async_register_http _LOGGER = logging.getLogger(__name__) @@ -43,34 +41,28 @@ ENTITY_SCHEMA = vol.Schema({ vol.Optional(CONF_ROOM_HINT): cv.string }) -CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: { - vol.Required(CONF_PROJECT_ID): cv.string, - vol.Required(CONF_CLIENT_ID): cv.string, - vol.Required(CONF_ACCESS_TOKEN): cv.string, - vol.Optional(CONF_EXPOSE_BY_DEFAULT, - default=DEFAULT_EXPOSE_BY_DEFAULT): cv.boolean, - vol.Optional(CONF_EXPOSED_DOMAINS, - default=DEFAULT_EXPOSED_DOMAINS): cv.ensure_list, - vol.Optional(CONF_AGENT_USER_ID, - default=DEFAULT_AGENT_USER_ID): cv.string, - vol.Optional(CONF_API_KEY): cv.string, - vol.Optional(CONF_ENTITY_CONFIG): {cv.entity_id: ENTITY_SCHEMA} - } - }, - extra=vol.ALLOW_EXTRA) +GOOGLE_ASSISTANT_SCHEMA = vol.Schema({ + vol.Required(CONF_PROJECT_ID): cv.string, + vol.Optional(CONF_EXPOSE_BY_DEFAULT, + default=DEFAULT_EXPOSE_BY_DEFAULT): cv.boolean, + vol.Optional(CONF_EXPOSED_DOMAINS, + default=DEFAULT_EXPOSED_DOMAINS): cv.ensure_list, + vol.Optional(CONF_API_KEY): cv.string, + vol.Optional(CONF_ENTITY_CONFIG): {cv.entity_id: ENTITY_SCHEMA} +}, extra=vol.PREVENT_EXTRA) + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: GOOGLE_ASSISTANT_SCHEMA +}, extra=vol.ALLOW_EXTRA) async def async_setup(hass: HomeAssistant, yaml_config: Dict[str, Any]): """Activate Google Actions component.""" config = yaml_config.get(DOMAIN, {}) - agent_user_id = config.get(CONF_AGENT_USER_ID) api_key = config.get(CONF_API_KEY) - hass.http.register_view(GoogleAssistantAuthView(hass, config)) async_register_http(hass, config) - async def request_sync_service_handler(call): + async def request_sync_service_handler(call: ServiceCall): """Handle request sync service calls.""" websession = async_get_clientsession(hass) try: @@ -78,7 +70,7 @@ async def async_setup(hass: HomeAssistant, yaml_config: Dict[str, Any]): res = await websession.post( REQUEST_SYNC_BASE_URL, params={'key': api_key}, - json={'agent_user_id': agent_user_id}) + json={'agent_user_id': call.context.user_id}) _LOGGER.info("Submitted request_sync request to Google") res.raise_for_status() except aiohttp.ClientResponseError: diff --git a/homeassistant/components/google_assistant/auth.py b/homeassistant/components/google_assistant/auth.py deleted file mode 100644 index 5b98e25014d..00000000000 --- a/homeassistant/components/google_assistant/auth.py +++ /dev/null @@ -1,83 +0,0 @@ -"""Google Assistant OAuth View.""" - -import logging -from typing import Dict, Any - -# Typing imports -# if False: -from aiohttp.web import Request, Response - -from homeassistant.core import HomeAssistant -from homeassistant.components.http import HomeAssistantView -from homeassistant.const import ( - HTTP_BAD_REQUEST, - HTTP_UNAUTHORIZED, - HTTP_MOVED_PERMANENTLY, -) - -from .const import ( - GOOGLE_ASSISTANT_API_ENDPOINT, - CONF_PROJECT_ID, CONF_CLIENT_ID, CONF_ACCESS_TOKEN -) - -BASE_OAUTH_URL = 'https://oauth-redirect.googleusercontent.com' -REDIRECT_TEMPLATE_URL = \ - '{}/r/{}#access_token={}&token_type=bearer&state={}' - -_LOGGER = logging.getLogger(__name__) - - -class GoogleAssistantAuthView(HomeAssistantView): - """Handle Google Actions auth requests.""" - - url = GOOGLE_ASSISTANT_API_ENDPOINT + '/auth' - name = 'api:google_assistant:auth' - requires_auth = False - - def __init__(self, hass: HomeAssistant, cfg: Dict[str, Any]) -> None: - """Initialize instance of the view.""" - super().__init__() - - self.project_id = cfg.get(CONF_PROJECT_ID) - self.client_id = cfg.get(CONF_CLIENT_ID) - self.access_token = cfg.get(CONF_ACCESS_TOKEN) - - async def get(self, request: Request) -> Response: - """Handle oauth token request.""" - query = request.query - redirect_uri = query.get('redirect_uri') - if not redirect_uri: - msg = 'missing redirect_uri field' - _LOGGER.warning(msg) - return self.json_message(msg, status_code=HTTP_BAD_REQUEST) - - if self.project_id not in redirect_uri: - msg = 'missing project_id in redirect_uri' - _LOGGER.warning(msg) - return self.json_message(msg, status_code=HTTP_BAD_REQUEST) - - state = query.get('state') - if not state: - msg = 'oauth request missing state' - _LOGGER.warning(msg) - return self.json_message(msg, status_code=HTTP_BAD_REQUEST) - - client_id = query.get('client_id') - if self.client_id != client_id: - msg = 'invalid client id' - _LOGGER.warning(msg) - return self.json_message(msg, status_code=HTTP_UNAUTHORIZED) - - generated_url = redirect_url(self.project_id, self.access_token, state) - - _LOGGER.info('user login in from Google Assistant') - return self.json_message( - 'redirect success', - status_code=HTTP_MOVED_PERMANENTLY, - headers={'Location': generated_url}) - - -def redirect_url(project_id: str, access_token: str, state: str) -> str: - """Generate the redirect format for the oauth request.""" - return REDIRECT_TEMPLATE_URL.format(BASE_OAUTH_URL, project_id, - access_token, state) diff --git a/homeassistant/components/google_assistant/const.py b/homeassistant/components/google_assistant/const.py index 12888ea2cf6..485b98e8e22 100644 --- a/homeassistant/components/google_assistant/const.py +++ b/homeassistant/components/google_assistant/const.py @@ -8,10 +8,7 @@ CONF_ENTITY_CONFIG = 'entity_config' CONF_EXPOSE_BY_DEFAULT = 'expose_by_default' CONF_EXPOSED_DOMAINS = 'exposed_domains' CONF_PROJECT_ID = 'project_id' -CONF_ACCESS_TOKEN = 'access_token' -CONF_CLIENT_ID = 'client_id' CONF_ALIASES = 'aliases' -CONF_AGENT_USER_ID = 'agent_user_id' CONF_API_KEY = 'api_key' CONF_ROOM_HINT = 'room' diff --git a/homeassistant/components/google_assistant/http.py b/homeassistant/components/google_assistant/http.py index 05bc3cbd01c..65af7b932b0 100644 --- a/homeassistant/components/google_assistant/http.py +++ b/homeassistant/components/google_assistant/http.py @@ -6,7 +6,6 @@ https://home-assistant.io/components/google_assistant/ """ import logging -from aiohttp.hdrs import AUTHORIZATION from aiohttp.web import Request, Response # Typing imports @@ -15,10 +14,8 @@ from homeassistant.core import callback from .const import ( GOOGLE_ASSISTANT_API_ENDPOINT, - CONF_ACCESS_TOKEN, CONF_EXPOSE_BY_DEFAULT, CONF_EXPOSED_DOMAINS, - CONF_AGENT_USER_ID, CONF_ENTITY_CONFIG, CONF_EXPOSE, ) @@ -31,10 +28,8 @@ _LOGGER = logging.getLogger(__name__) @callback def async_register_http(hass, cfg): """Register HTTP views for Google Assistant.""" - access_token = cfg.get(CONF_ACCESS_TOKEN) expose_by_default = cfg.get(CONF_EXPOSE_BY_DEFAULT) exposed_domains = cfg.get(CONF_EXPOSED_DOMAINS) - agent_user_id = cfg.get(CONF_AGENT_USER_ID) entity_config = cfg.get(CONF_ENTITY_CONFIG) or {} def is_exposed(entity) -> bool: @@ -57,9 +52,8 @@ def async_register_http(hass, cfg): return is_default_exposed or explicit_expose - gass_config = Config(is_exposed, agent_user_id, entity_config) hass.http.register_view( - GoogleAssistantView(access_token, gass_config)) + GoogleAssistantView(is_exposed, entity_config)) class GoogleAssistantView(HomeAssistantView): @@ -67,20 +61,19 @@ class GoogleAssistantView(HomeAssistantView): url = GOOGLE_ASSISTANT_API_ENDPOINT name = 'api:google_assistant' - requires_auth = False # Uses access token from oauth flow + requires_auth = True - def __init__(self, access_token, gass_config): + def __init__(self, is_exposed, entity_config): """Initialize the Google Assistant request handler.""" - self.access_token = access_token - self.gass_config = gass_config + self.is_exposed = is_exposed + self.entity_config = entity_config async def post(self, request: Request) -> Response: """Handle Google Assistant requests.""" - auth = request.headers.get(AUTHORIZATION, None) - if 'Bearer {}'.format(self.access_token) != auth: - return self.json_message("missing authorization", status_code=401) - message = await request.json() # type: dict + config = Config(self.is_exposed, + request['hass_user'].id, + self.entity_config) result = await async_handle_message( - request.app['hass'], self.gass_config, message) + request.app['hass'], config, message) return self.json(result) diff --git a/tests/components/google_assistant/test_google_assistant.py b/tests/components/google_assistant/test_google_assistant.py index d9682940bdc..2ebfa5cc9ed 100644 --- a/tests/components/google_assistant/test_google_assistant.py +++ b/tests/components/google_assistant/test_google_assistant.py @@ -23,7 +23,12 @@ HA_HEADERS = { PROJECT_ID = 'hasstest-1234' CLIENT_ID = 'helloworld' ACCESS_TOKEN = 'superdoublesecret' -AUTH_HEADER = {AUTHORIZATION: 'Bearer {}'.format(ACCESS_TOKEN)} + + +@pytest.fixture +def auth_header(hass_access_token): + """Generate an HTTP header with bearer token authorization.""" + return {AUTHORIZATION: 'Bearer {}'.format(hass_access_token)} @pytest.fixture @@ -33,8 +38,6 @@ def assistant_client(loop, hass, aiohttp_client): setup.async_setup_component(hass, 'google_assistant', { 'google_assistant': { 'project_id': PROJECT_ID, - 'client_id': CLIENT_ID, - 'access_token': ACCESS_TOKEN, 'entity_config': { 'light.ceiling_lights': { 'aliases': ['top lights', 'ceiling lights'], @@ -97,31 +100,14 @@ def hass_fixture(loop, hass): @asyncio.coroutine -def test_auth(assistant_client): - """Test the auth process.""" - result = yield from assistant_client.get( - ga.const.GOOGLE_ASSISTANT_API_ENDPOINT + '/auth', - params={ - 'redirect_uri': - 'http://testurl/r/{}'.format(PROJECT_ID), - 'client_id': CLIENT_ID, - 'state': 'random1234', - }, - allow_redirects=False) - assert result.status == 301 - loc = result.headers.get('Location') - assert ACCESS_TOKEN in loc - - -@asyncio.coroutine -def test_sync_request(hass_fixture, assistant_client): +def test_sync_request(hass_fixture, assistant_client, auth_header): """Test a sync request.""" reqid = '5711642932632160983' data = {'requestId': reqid, 'inputs': [{'intent': 'action.devices.SYNC'}]} result = yield from assistant_client.post( ga.const.GOOGLE_ASSISTANT_API_ENDPOINT, data=json.dumps(data), - headers=AUTH_HEADER) + headers=auth_header) assert result.status == 200 body = yield from result.json() assert body.get('requestId') == reqid @@ -141,7 +127,7 @@ def test_sync_request(hass_fixture, assistant_client): @asyncio.coroutine -def test_query_request(hass_fixture, assistant_client): +def test_query_request(hass_fixture, assistant_client, auth_header): """Test a query request.""" reqid = '5711642932632160984' data = { @@ -165,7 +151,7 @@ def test_query_request(hass_fixture, assistant_client): result = yield from assistant_client.post( ga.const.GOOGLE_ASSISTANT_API_ENDPOINT, data=json.dumps(data), - headers=AUTH_HEADER) + headers=auth_header) assert result.status == 200 body = yield from result.json() assert body.get('requestId') == reqid @@ -180,7 +166,7 @@ def test_query_request(hass_fixture, assistant_client): @asyncio.coroutine -def test_query_climate_request(hass_fixture, assistant_client): +def test_query_climate_request(hass_fixture, assistant_client, auth_header): """Test a query request.""" reqid = '5711642932632160984' data = { @@ -200,7 +186,7 @@ def test_query_climate_request(hass_fixture, assistant_client): result = yield from assistant_client.post( ga.const.GOOGLE_ASSISTANT_API_ENDPOINT, data=json.dumps(data), - headers=AUTH_HEADER) + headers=auth_header) assert result.status == 200 body = yield from result.json() assert body.get('requestId') == reqid @@ -229,7 +215,7 @@ def test_query_climate_request(hass_fixture, assistant_client): @asyncio.coroutine -def test_query_climate_request_f(hass_fixture, assistant_client): +def test_query_climate_request_f(hass_fixture, assistant_client, auth_header): """Test a query request.""" # Mock demo devices as fahrenheit to see if we convert to celsius hass_fixture.config.units.temperature_unit = const.TEMP_FAHRENHEIT @@ -256,7 +242,7 @@ def test_query_climate_request_f(hass_fixture, assistant_client): result = yield from assistant_client.post( ga.const.GOOGLE_ASSISTANT_API_ENDPOINT, data=json.dumps(data), - headers=AUTH_HEADER) + headers=auth_header) assert result.status == 200 body = yield from result.json() assert body.get('requestId') == reqid @@ -286,7 +272,7 @@ def test_query_climate_request_f(hass_fixture, assistant_client): @asyncio.coroutine -def test_execute_request(hass_fixture, assistant_client): +def test_execute_request(hass_fixture, assistant_client, auth_header): """Test an execute request.""" reqid = '5711642932632160985' data = { @@ -358,7 +344,7 @@ def test_execute_request(hass_fixture, assistant_client): result = yield from assistant_client.post( ga.const.GOOGLE_ASSISTANT_API_ENDPOINT, data=json.dumps(data), - headers=AUTH_HEADER) + headers=auth_header) assert result.status == 200 body = yield from result.json() assert body.get('requestId') == reqid diff --git a/tests/components/google_assistant/test_init.py b/tests/components/google_assistant/test_init.py index 9ced9fc329d..3f6a799b423 100644 --- a/tests/components/google_assistant/test_init.py +++ b/tests/components/google_assistant/test_init.py @@ -5,7 +5,6 @@ from homeassistant.setup import async_setup_component from homeassistant.components import google_assistant as ga GA_API_KEY = "Agdgjsj399sdfkosd932ksd" -GA_AGENT_USER_ID = "testid" @asyncio.coroutine @@ -17,9 +16,6 @@ def test_request_sync_service(aioclient_mock, hass): yield from async_setup_component(hass, 'google_assistant', { 'google_assistant': { 'project_id': 'test_project', - 'client_id': 'r7328kwdsdfsdf03223409', - 'access_token': '8wdsfjsf932492342349234', - 'agent_user_id': GA_AGENT_USER_ID, 'api_key': GA_API_KEY }}) From bab079f6493b1ffa8356e8d4bc5a6d2c4293d634 Mon Sep 17 00:00:00 2001 From: Charles Garwood Date: Wed, 26 Sep 2018 03:19:47 -0400 Subject: [PATCH 062/247] Add unique_id to Nest Sensors (#16869) * Add unique_id * Add device_info * Fix typo * Update __init__.py --- homeassistant/components/nest/__init__.py | 31 +++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/homeassistant/components/nest/__init__.py b/homeassistant/components/nest/__init__.py index 57111350396..f609c774b12 100644 --- a/homeassistant/components/nest/__init__.py +++ b/homeassistant/components/nest/__init__.py @@ -309,6 +309,37 @@ class NestSensorDevice(Entity): """Do not need poll thanks using Nest streaming API.""" return False + @property + def unique_id(self): + """Return unique id based on device serial and variable.""" + return "{}-{}".format(self.device.serial, self.variable) + + @property + def device_info(self): + """Return information about the device.""" + if not hasattr(self.device, 'name_long'): + name = self.structure.name + model = "Structure" + else: + name = self.device.name_long + if self.device.is_thermostat: + model = 'Thermostat' + elif self.device.is_camera: + model = 'Camera' + elif self.device.is_smoke_co_alarm: + model = 'Nest Protect' + else: + model = None + + return { + 'identifiers': { + (DOMAIN, self.device.serial) + }, + 'name': name, + 'manufacturer': 'Nest Labs', + 'model': model, + } + def update(self): """Do not use NestSensorDevice directly.""" raise NotImplementedError From c899875abb543b37350492c7b0636dea572c21c5 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 26 Sep 2018 09:38:50 +0200 Subject: [PATCH 063/247] Fix MQTT discovery (#16864) * Fix MQTT discovery * Update __init__.py --- homeassistant/components/mqtt/__init__.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 856d5d01894..70f20453633 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -21,7 +21,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import ( CONF_PASSWORD, CONF_PAYLOAD, CONF_PORT, CONF_PROTOCOL, CONF_USERNAME, - CONF_VALUE_TEMPLATE, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) + CONF_VALUE_TEMPLATE, EVENT_HOMEASSISTANT_STOP) from homeassistant.core import Event, ServiceCall, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv @@ -524,12 +524,8 @@ async def async_setup_entry(hass, entry): schema=MQTT_PUBLISH_SCHEMA) if conf.get(CONF_DISCOVERY): - async def async_setup_discovery(event): - await _async_setup_discovery( - hass, conf, hass.data[DATA_MQTT_HASS_CONFIG]) - - hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_START, async_setup_discovery) + await _async_setup_discovery( + hass, conf, hass.data[DATA_MQTT_HASS_CONFIG]) return True From fa98a27df77849ddf0395a86f56ac736bc6c2e81 Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Wed, 26 Sep 2018 09:49:55 +0200 Subject: [PATCH 064/247] Remove service helper (2) (#16863) * alarm_control_panel * automation * camera * climate * counter * fan * Add legacy notes * Fix tests --- .../alarm_control_panel/__init__.py | 73 ----------- .../components/automation/__init__.py | 28 ----- homeassistant/components/camera/__init__.py | 56 --------- homeassistant/components/climate/__init__.py | 104 +--------------- homeassistant/components/counter/__init__.py | 44 ------- homeassistant/components/fan/__init__.py | 71 ----------- .../components/alarm_control_panel/common.py | 83 +++++++++++++ .../alarm_control_panel/test_manual.py | 105 ++++++++-------- .../alarm_control_panel/test_manual_mqtt.py | 103 ++++++++-------- .../alarm_control_panel/test_mqtt.py | 13 +- tests/components/automation/common.py | 53 ++++++++ tests/components/automation/test_event.py | 5 +- tests/components/automation/test_init.py | 19 +-- tests/components/automation/test_litejet.py | 3 +- tests/components/automation/test_mqtt.py | 3 +- .../automation/test_numeric_state.py | 5 +- tests/components/automation/test_state.py | 5 +- tests/components/automation/test_sun.py | 5 +- tests/components/automation/test_template.py | 3 +- tests/components/automation/test_time.py | 3 +- tests/components/automation/test_zone.py | 3 +- tests/components/camera/common.py | 68 +++++++++++ tests/components/camera/test_demo.py | 12 +- tests/components/camera/test_init.py | 3 +- tests/components/climate/common.py | 115 ++++++++++++++++++ tests/components/climate/test_demo.py | 51 ++++---- .../climate/test_generic_thermostat.py | 113 ++++++++--------- tests/components/climate/test_mqtt.py | 59 ++++----- tests/components/counter/common.py | 52 ++++++++ tests/components/counter/test_init.py | 4 +- tests/components/fan/common.py | 82 +++++++++++++ tests/components/fan/test_demo.py | 23 ++-- tests/components/fan/test_template.py | 63 +++++----- 33 files changed, 766 insertions(+), 666 deletions(-) create mode 100644 tests/components/alarm_control_panel/common.py create mode 100644 tests/components/automation/common.py create mode 100644 tests/components/camera/common.py create mode 100644 tests/components/climate/common.py create mode 100644 tests/components/counter/common.py create mode 100644 tests/components/fan/common.py diff --git a/homeassistant/components/alarm_control_panel/__init__.py b/homeassistant/components/alarm_control_panel/__init__.py index 63977ed88c7..c5110a2ad5a 100644 --- a/homeassistant/components/alarm_control_panel/__init__.py +++ b/homeassistant/components/alarm_control_panel/__init__.py @@ -14,7 +14,6 @@ from homeassistant.const import ( ATTR_CODE, ATTR_CODE_FORMAT, ATTR_ENTITY_ID, SERVICE_ALARM_TRIGGER, SERVICE_ALARM_DISARM, SERVICE_ALARM_ARM_HOME, SERVICE_ALARM_ARM_AWAY, SERVICE_ALARM_ARM_NIGHT, SERVICE_ALARM_ARM_CUSTOM_BYPASS) -from homeassistant.loader import bind_hass from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity @@ -32,78 +31,6 @@ ALARM_SERVICE_SCHEMA = vol.Schema({ }) -@bind_hass -def alarm_disarm(hass, code=None, entity_id=None): - """Send the alarm the command for disarm.""" - data = {} - if code: - data[ATTR_CODE] = code - if entity_id: - data[ATTR_ENTITY_ID] = entity_id - - hass.services.call(DOMAIN, SERVICE_ALARM_DISARM, data) - - -@bind_hass -def alarm_arm_home(hass, code=None, entity_id=None): - """Send the alarm the command for arm home.""" - data = {} - if code: - data[ATTR_CODE] = code - if entity_id: - data[ATTR_ENTITY_ID] = entity_id - - hass.services.call(DOMAIN, SERVICE_ALARM_ARM_HOME, data) - - -@bind_hass -def alarm_arm_away(hass, code=None, entity_id=None): - """Send the alarm the command for arm away.""" - data = {} - if code: - data[ATTR_CODE] = code - if entity_id: - data[ATTR_ENTITY_ID] = entity_id - - hass.services.call(DOMAIN, SERVICE_ALARM_ARM_AWAY, data) - - -@bind_hass -def alarm_arm_night(hass, code=None, entity_id=None): - """Send the alarm the command for arm night.""" - data = {} - if code: - data[ATTR_CODE] = code - if entity_id: - data[ATTR_ENTITY_ID] = entity_id - - hass.services.call(DOMAIN, SERVICE_ALARM_ARM_NIGHT, data) - - -@bind_hass -def alarm_trigger(hass, code=None, entity_id=None): - """Send the alarm the command for trigger.""" - data = {} - if code: - data[ATTR_CODE] = code - if entity_id: - data[ATTR_ENTITY_ID] = entity_id - - hass.services.call(DOMAIN, SERVICE_ALARM_TRIGGER, data) - - -@bind_hass -def alarm_arm_custom_bypass(hass, code=None, entity_id=None): - """Send the alarm the command for arm custom bypass.""" - data = {} - if code: - data[ATTR_CODE] = code - if entity_id: - data[ATTR_ENTITY_ID] = entity_id - - hass.services.call(DOMAIN, SERVICE_ALARM_ARM_CUSTOM_BYPASS, data) - - @asyncio.coroutine def async_setup(hass, config): """Track states and offer events for sensors.""" diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 43fd4cedb88..e657409ea01 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -115,34 +115,6 @@ def is_on(hass, entity_id): return hass.states.is_state(entity_id, STATE_ON) -@bind_hass -def turn_on(hass, entity_id=None): - """Turn on specified automation or all.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} - hass.services.call(DOMAIN, SERVICE_TURN_ON, data) - - -@bind_hass -def turn_off(hass, entity_id=None): - """Turn off specified automation or all.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} - hass.services.call(DOMAIN, SERVICE_TURN_OFF, data) - - -@bind_hass -def toggle(hass, entity_id=None): - """Toggle specified automation or all.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} - hass.services.call(DOMAIN, SERVICE_TOGGLE, data) - - -@bind_hass -def trigger(hass, entity_id=None): - """Trigger specified automation or all.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} - hass.services.call(DOMAIN, SERVICE_TRIGGER, data) - - @bind_hass def reload(hass): """Reload the automation from config.""" diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index a8a486013d4..b32236e499d 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -83,62 +83,6 @@ class Image: content = attr.ib(type=bytes) -@bind_hass -def turn_off(hass, entity_id=None): - """Turn off camera.""" - hass.add_job(async_turn_off, hass, entity_id) - - -@bind_hass -async def async_turn_off(hass, entity_id=None): - """Turn off camera.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} - await hass.services.async_call(DOMAIN, SERVICE_TURN_OFF, data) - - -@bind_hass -def turn_on(hass, entity_id=None): - """Turn on camera.""" - hass.add_job(async_turn_on, hass, entity_id) - - -@bind_hass -async def async_turn_on(hass, entity_id=None): - """Turn on camera, and set operation mode.""" - data = {} - if entity_id is not None: - data[ATTR_ENTITY_ID] = entity_id - - await hass.services.async_call(DOMAIN, SERVICE_TURN_ON, data) - - -@bind_hass -def enable_motion_detection(hass, entity_id=None): - """Enable Motion Detection.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else None - hass.async_add_job(hass.services.async_call( - DOMAIN, SERVICE_ENABLE_MOTION, data)) - - -@bind_hass -def disable_motion_detection(hass, entity_id=None): - """Disable Motion Detection.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else None - hass.async_add_job(hass.services.async_call( - DOMAIN, SERVICE_DISABLE_MOTION, data)) - - -@bind_hass -@callback -def async_snapshot(hass, filename, entity_id=None): - """Make a snapshot from a camera.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} - data[ATTR_FILENAME] = filename - - hass.async_add_job(hass.services.async_call( - DOMAIN, SERVICE_SNAPSHOT, data)) - - @bind_hass async def async_get_image(hass, entity_id, timeout=10): """Fetch an image from a camera entity.""" diff --git a/homeassistant/components/climate/__init__.py b/homeassistant/components/climate/__init__.py index a3273f67cc2..98483c454bc 100644 --- a/homeassistant/components/climate/__init__.py +++ b/homeassistant/components/climate/__init__.py @@ -10,7 +10,6 @@ import functools as ft import voluptuous as vol -from homeassistant.loader import bind_hass from homeassistant.helpers.temperature import display_temp as show_temp from homeassistant.util.temperature import convert as convert_temperature from homeassistant.helpers.entity_component import EntityComponent @@ -20,7 +19,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_TEMPERATURE, SERVICE_TURN_ON, SERVICE_TURN_OFF, STATE_ON, STATE_OFF, STATE_UNKNOWN, TEMP_CELSIUS, PRECISION_WHOLE, - PRECISION_TENTHS, ) + PRECISION_TENTHS) DEFAULT_MIN_TEMP = 7 DEFAULT_MAX_TEMP = 35 @@ -142,107 +141,6 @@ SET_SWING_MODE_SCHEMA = vol.Schema({ }) -@bind_hass -def set_away_mode(hass, away_mode, entity_id=None): - """Turn all or specified climate devices away mode on.""" - data = { - ATTR_AWAY_MODE: away_mode - } - - if entity_id: - data[ATTR_ENTITY_ID] = entity_id - - hass.services.call(DOMAIN, SERVICE_SET_AWAY_MODE, data) - - -@bind_hass -def set_hold_mode(hass, hold_mode, entity_id=None): - """Set new hold mode.""" - data = { - ATTR_HOLD_MODE: hold_mode - } - - if entity_id: - data[ATTR_ENTITY_ID] = entity_id - - hass.services.call(DOMAIN, SERVICE_SET_HOLD_MODE, data) - - -@bind_hass -def set_aux_heat(hass, aux_heat, entity_id=None): - """Turn all or specified climate devices auxiliary heater on.""" - data = { - ATTR_AUX_HEAT: aux_heat - } - - if entity_id: - data[ATTR_ENTITY_ID] = entity_id - - hass.services.call(DOMAIN, SERVICE_SET_AUX_HEAT, data) - - -@bind_hass -def set_temperature(hass, temperature=None, entity_id=None, - target_temp_high=None, target_temp_low=None, - operation_mode=None): - """Set new target temperature.""" - kwargs = { - key: value for key, value in [ - (ATTR_TEMPERATURE, temperature), - (ATTR_TARGET_TEMP_HIGH, target_temp_high), - (ATTR_TARGET_TEMP_LOW, target_temp_low), - (ATTR_ENTITY_ID, entity_id), - (ATTR_OPERATION_MODE, operation_mode) - ] if value is not None - } - _LOGGER.debug("set_temperature start data=%s", kwargs) - hass.services.call(DOMAIN, SERVICE_SET_TEMPERATURE, kwargs) - - -@bind_hass -def set_humidity(hass, humidity, entity_id=None): - """Set new target humidity.""" - data = {ATTR_HUMIDITY: humidity} - - if entity_id is not None: - data[ATTR_ENTITY_ID] = entity_id - - hass.services.call(DOMAIN, SERVICE_SET_HUMIDITY, data) - - -@bind_hass -def set_fan_mode(hass, fan, entity_id=None): - """Set all or specified climate devices fan mode on.""" - data = {ATTR_FAN_MODE: fan} - - if entity_id: - data[ATTR_ENTITY_ID] = entity_id - - hass.services.call(DOMAIN, SERVICE_SET_FAN_MODE, data) - - -@bind_hass -def set_operation_mode(hass, operation_mode, entity_id=None): - """Set new target operation mode.""" - data = {ATTR_OPERATION_MODE: operation_mode} - - if entity_id is not None: - data[ATTR_ENTITY_ID] = entity_id - - hass.services.call(DOMAIN, SERVICE_SET_OPERATION_MODE, data) - - -@bind_hass -def set_swing_mode(hass, swing_mode, entity_id=None): - """Set new target swing mode.""" - data = {ATTR_SWING_MODE: swing_mode} - - if entity_id is not None: - data[ATTR_ENTITY_ID] = entity_id - - hass.services.call(DOMAIN, SERVICE_SET_SWING_MODE, data) - - async def async_setup(hass, config): """Set up climate devices.""" component = hass.data[DOMAIN] = \ diff --git a/homeassistant/components/counter/__init__.py b/homeassistant/components/counter/__init__.py index d720819a0ab..9ef4d4374ce 100644 --- a/homeassistant/components/counter/__init__.py +++ b/homeassistant/components/counter/__init__.py @@ -9,12 +9,10 @@ import logging import voluptuous as vol from homeassistant.const import ATTR_ENTITY_ID, CONF_ICON, CONF_NAME -from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.restore_state import async_get_last_state -from homeassistant.loader import bind_hass _LOGGER = logging.getLogger(__name__) @@ -51,48 +49,6 @@ CONFIG_SCHEMA = vol.Schema({ }, extra=vol.ALLOW_EXTRA) -@bind_hass -def increment(hass, entity_id): - """Increment a counter.""" - hass.add_job(async_increment, hass, entity_id) - - -@callback -@bind_hass -def async_increment(hass, entity_id): - """Increment a counter.""" - hass.async_add_job(hass.services.async_call( - DOMAIN, SERVICE_INCREMENT, {ATTR_ENTITY_ID: entity_id})) - - -@bind_hass -def decrement(hass, entity_id): - """Decrement a counter.""" - hass.add_job(async_decrement, hass, entity_id) - - -@callback -@bind_hass -def async_decrement(hass, entity_id): - """Decrement a counter.""" - hass.async_add_job(hass.services.async_call( - DOMAIN, SERVICE_DECREMENT, {ATTR_ENTITY_ID: entity_id})) - - -@bind_hass -def reset(hass, entity_id): - """Reset a counter.""" - hass.add_job(async_reset, hass, entity_id) - - -@callback -@bind_hass -def async_reset(hass, entity_id): - """Reset a counter.""" - hass.async_add_job(hass.services.async_call( - DOMAIN, SERVICE_RESET, {ATTR_ENTITY_ID: entity_id})) - - async def async_setup(hass, config): """Set up the counters.""" component = EntityComponent(_LOGGER, DOMAIN, hass) diff --git a/homeassistant/components/fan/__init__.py b/homeassistant/components/fan/__init__.py index f2704e84bc5..ec18637f065 100644 --- a/homeassistant/components/fan/__init__.py +++ b/homeassistant/components/fan/__init__.py @@ -98,77 +98,6 @@ def is_on(hass, entity_id: str = None) -> bool: return state.attributes[ATTR_SPEED] not in [SPEED_OFF, STATE_UNKNOWN] -@bind_hass -def turn_on(hass, entity_id: str = None, speed: str = None) -> None: - """Turn all or specified fan on.""" - data = { - key: value for key, value in [ - (ATTR_ENTITY_ID, entity_id), - (ATTR_SPEED, speed), - ] if value is not None - } - - hass.services.call(DOMAIN, SERVICE_TURN_ON, data) - - -@bind_hass -def turn_off(hass, entity_id: str = None) -> None: - """Turn all or specified fan off.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} - - hass.services.call(DOMAIN, SERVICE_TURN_OFF, data) - - -@bind_hass -def toggle(hass, entity_id: str = None) -> None: - """Toggle all or specified fans.""" - data = { - ATTR_ENTITY_ID: entity_id - } - - hass.services.call(DOMAIN, SERVICE_TOGGLE, data) - - -@bind_hass -def oscillate(hass, entity_id: str = None, - should_oscillate: bool = True) -> None: - """Set oscillation on all or specified fan.""" - data = { - key: value for key, value in [ - (ATTR_ENTITY_ID, entity_id), - (ATTR_OSCILLATING, should_oscillate), - ] if value is not None - } - - hass.services.call(DOMAIN, SERVICE_OSCILLATE, data) - - -@bind_hass -def set_speed(hass, entity_id: str = None, speed: str = None) -> None: - """Set speed for all or specified fan.""" - data = { - key: value for key, value in [ - (ATTR_ENTITY_ID, entity_id), - (ATTR_SPEED, speed), - ] if value is not None - } - - hass.services.call(DOMAIN, SERVICE_SET_SPEED, data) - - -@bind_hass -def set_direction(hass, entity_id: str = None, direction: str = None) -> None: - """Set direction for all or specified fan.""" - data = { - key: value for key, value in [ - (ATTR_ENTITY_ID, entity_id), - (ATTR_DIRECTION, direction), - ] if value is not None - } - - hass.services.call(DOMAIN, SERVICE_SET_DIRECTION, data) - - @asyncio.coroutine def async_setup(hass, config: dict): """Expose fan control via statemachine and services.""" diff --git a/tests/components/alarm_control_panel/common.py b/tests/components/alarm_control_panel/common.py new file mode 100644 index 00000000000..cf2de857076 --- /dev/null +++ b/tests/components/alarm_control_panel/common.py @@ -0,0 +1,83 @@ +"""Collection of helper methods. + +All containing methods are legacy helpers that should not be used by new +components. Instead call the service directly. +""" +from homeassistant.components.alarm_control_panel import DOMAIN +from homeassistant.const import ( + ATTR_CODE, ATTR_ENTITY_ID, SERVICE_ALARM_TRIGGER, + SERVICE_ALARM_DISARM, SERVICE_ALARM_ARM_HOME, SERVICE_ALARM_ARM_AWAY, + SERVICE_ALARM_ARM_NIGHT, SERVICE_ALARM_ARM_CUSTOM_BYPASS) +from homeassistant.loader import bind_hass + + +@bind_hass +def alarm_disarm(hass, code=None, entity_id=None): + """Send the alarm the command for disarm.""" + data = {} + if code: + data[ATTR_CODE] = code + if entity_id: + data[ATTR_ENTITY_ID] = entity_id + + hass.services.call(DOMAIN, SERVICE_ALARM_DISARM, data) + + +@bind_hass +def alarm_arm_home(hass, code=None, entity_id=None): + """Send the alarm the command for arm home.""" + data = {} + if code: + data[ATTR_CODE] = code + if entity_id: + data[ATTR_ENTITY_ID] = entity_id + + hass.services.call(DOMAIN, SERVICE_ALARM_ARM_HOME, data) + + +@bind_hass +def alarm_arm_away(hass, code=None, entity_id=None): + """Send the alarm the command for arm away.""" + data = {} + if code: + data[ATTR_CODE] = code + if entity_id: + data[ATTR_ENTITY_ID] = entity_id + + hass.services.call(DOMAIN, SERVICE_ALARM_ARM_AWAY, data) + + +@bind_hass +def alarm_arm_night(hass, code=None, entity_id=None): + """Send the alarm the command for arm night.""" + data = {} + if code: + data[ATTR_CODE] = code + if entity_id: + data[ATTR_ENTITY_ID] = entity_id + + hass.services.call(DOMAIN, SERVICE_ALARM_ARM_NIGHT, data) + + +@bind_hass +def alarm_trigger(hass, code=None, entity_id=None): + """Send the alarm the command for trigger.""" + data = {} + if code: + data[ATTR_CODE] = code + if entity_id: + data[ATTR_ENTITY_ID] = entity_id + + hass.services.call(DOMAIN, SERVICE_ALARM_TRIGGER, data) + + +@bind_hass +def alarm_arm_custom_bypass(hass, code=None, entity_id=None): + """Send the alarm the command for arm custom bypass.""" + data = {} + if code: + data[ATTR_CODE] = code + if entity_id: + data[ATTR_ENTITY_ID] = entity_id + + hass.services.call(DOMAIN, SERVICE_ALARM_ARM_CUSTOM_BYPASS, data) diff --git a/tests/components/alarm_control_panel/test_manual.py b/tests/components/alarm_control_panel/test_manual.py index 29f630093d9..02085a44b47 100644 --- a/tests/components/alarm_control_panel/test_manual.py +++ b/tests/components/alarm_control_panel/test_manual.py @@ -14,6 +14,7 @@ from homeassistant.components import alarm_control_panel import homeassistant.util.dt as dt_util from tests.common import fire_time_changed, get_test_home_assistant +from tests.components.alarm_control_panel import common CODE = 'HELLO_CODE' @@ -53,7 +54,7 @@ class TestAlarmControlPanelManual(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_arm_home(self.hass, CODE) + common.alarm_arm_home(self.hass, CODE) self.hass.block_till_done() self.assertEqual(STATE_ALARM_ARMED_HOME, @@ -76,7 +77,7 @@ class TestAlarmControlPanelManual(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_arm_home(self.hass, CODE, entity_id) + common.alarm_arm_home(self.hass, CODE, entity_id) self.hass.block_till_done() self.assertEqual(STATE_ALARM_PENDING, @@ -111,7 +112,7 @@ class TestAlarmControlPanelManual(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_arm_home(self.hass, CODE + '2') + common.alarm_arm_home(self.hass, CODE + '2') self.hass.block_till_done() self.assertEqual(STATE_ALARM_DISARMED, @@ -134,7 +135,7 @@ class TestAlarmControlPanelManual(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_arm_away(self.hass, CODE, entity_id) + common.alarm_arm_away(self.hass, CODE, entity_id) self.hass.block_till_done() self.assertEqual(STATE_ALARM_ARMED_AWAY, @@ -160,7 +161,7 @@ class TestAlarmControlPanelManual(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_arm_home(self.hass, 'abc') + common.alarm_arm_home(self.hass, 'abc') self.hass.block_till_done() state = self.hass.states.get(entity_id) @@ -183,7 +184,7 @@ class TestAlarmControlPanelManual(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_arm_away(self.hass, CODE) + common.alarm_arm_away(self.hass, CODE) self.hass.block_till_done() self.assertEqual(STATE_ALARM_PENDING, @@ -218,7 +219,7 @@ class TestAlarmControlPanelManual(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_arm_away(self.hass, CODE + '2') + common.alarm_arm_away(self.hass, CODE + '2') self.hass.block_till_done() self.assertEqual(STATE_ALARM_DISARMED, @@ -241,7 +242,7 @@ class TestAlarmControlPanelManual(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_arm_night(self.hass, CODE) + common.alarm_arm_night(self.hass, CODE) self.hass.block_till_done() self.assertEqual(STATE_ALARM_ARMED_NIGHT, @@ -264,7 +265,7 @@ class TestAlarmControlPanelManual(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_arm_night(self.hass, CODE, entity_id) + common.alarm_arm_night(self.hass, CODE, entity_id) self.hass.block_till_done() self.assertEqual(STATE_ALARM_PENDING, @@ -284,7 +285,7 @@ class TestAlarmControlPanelManual(unittest.TestCase): assert state.state == STATE_ALARM_ARMED_NIGHT # Do not go to the pending state when updating to the same state - alarm_control_panel.alarm_arm_night(self.hass, CODE, entity_id) + common.alarm_arm_night(self.hass, CODE, entity_id) self.hass.block_till_done() self.assertEqual(STATE_ALARM_ARMED_NIGHT, @@ -307,7 +308,7 @@ class TestAlarmControlPanelManual(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_arm_night(self.hass, CODE + '2') + common.alarm_arm_night(self.hass, CODE + '2') self.hass.block_till_done() self.assertEqual(STATE_ALARM_DISARMED, @@ -329,7 +330,7 @@ class TestAlarmControlPanelManual(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_trigger(self.hass, entity_id=entity_id) + common.alarm_trigger(self.hass, entity_id=entity_id) self.hass.block_till_done() self.assertEqual(STATE_ALARM_PENDING, @@ -362,13 +363,13 @@ class TestAlarmControlPanelManual(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_arm_away(self.hass, CODE) + common.alarm_arm_away(self.hass, CODE) self.hass.block_till_done() self.assertEqual(STATE_ALARM_ARMED_AWAY, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_trigger(self.hass, entity_id=entity_id) + common.alarm_trigger(self.hass, entity_id=entity_id) self.hass.block_till_done() state = self.hass.states.get(entity_id) @@ -402,7 +403,7 @@ class TestAlarmControlPanelManual(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_trigger(self.hass) + common.alarm_trigger(self.hass) self.hass.block_till_done() self.assertEqual(STATE_ALARM_DISARMED, @@ -425,7 +426,7 @@ class TestAlarmControlPanelManual(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_trigger(self.hass) + common.alarm_trigger(self.hass) self.hass.block_till_done() self.assertEqual(STATE_ALARM_DISARMED, @@ -448,7 +449,7 @@ class TestAlarmControlPanelManual(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_trigger(self.hass) + common.alarm_trigger(self.hass) self.hass.block_till_done() self.assertEqual(STATE_ALARM_PENDING, @@ -496,13 +497,13 @@ class TestAlarmControlPanelManual(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_arm_away(self.hass, CODE) + common.alarm_arm_away(self.hass, CODE) self.hass.block_till_done() self.assertEqual(STATE_ALARM_ARMED_AWAY, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_trigger(self.hass, entity_id=entity_id) + common.alarm_trigger(self.hass, entity_id=entity_id) self.hass.block_till_done() state = self.hass.states.get(entity_id) @@ -540,13 +541,13 @@ class TestAlarmControlPanelManual(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_arm_away(self.hass, CODE) + common.alarm_arm_away(self.hass, CODE) self.hass.block_till_done() self.assertEqual(STATE_ALARM_ARMED_AWAY, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_trigger(self.hass, entity_id=entity_id) + common.alarm_trigger(self.hass, entity_id=entity_id) self.hass.block_till_done() state = self.hass.states.get(entity_id) @@ -584,13 +585,13 @@ class TestAlarmControlPanelManual(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_arm_away(self.hass, CODE) + common.alarm_arm_away(self.hass, CODE) self.hass.block_till_done() self.assertEqual(STATE_ALARM_ARMED_AWAY, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_trigger(self.hass, entity_id=entity_id) + common.alarm_trigger(self.hass, entity_id=entity_id) self.hass.block_till_done() state = self.hass.states.get(entity_id) @@ -640,13 +641,13 @@ class TestAlarmControlPanelManual(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_arm_away(self.hass, CODE) + common.alarm_arm_away(self.hass, CODE) self.hass.block_till_done() self.assertEqual(STATE_ALARM_ARMED_AWAY, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_trigger(self.hass, entity_id=entity_id) + common.alarm_trigger(self.hass, entity_id=entity_id) self.hass.block_till_done() state = self.hass.states.get(entity_id) @@ -687,7 +688,7 @@ class TestAlarmControlPanelManual(unittest.TestCase): entity_id = 'alarm_control_panel.test' - alarm_control_panel.alarm_arm_home(self.hass) + common.alarm_arm_home(self.hass) self.hass.block_till_done() self.assertEqual(STATE_ALARM_PENDING, @@ -717,7 +718,7 @@ class TestAlarmControlPanelManual(unittest.TestCase): entity_id = 'alarm_control_panel.test' - alarm_control_panel.alarm_arm_away(self.hass) + common.alarm_arm_away(self.hass) self.hass.block_till_done() self.assertEqual(STATE_ALARM_PENDING, @@ -747,7 +748,7 @@ class TestAlarmControlPanelManual(unittest.TestCase): entity_id = 'alarm_control_panel.test' - alarm_control_panel.alarm_arm_night(self.hass) + common.alarm_arm_night(self.hass) self.hass.block_till_done() self.assertEqual(STATE_ALARM_PENDING, @@ -779,7 +780,7 @@ class TestAlarmControlPanelManual(unittest.TestCase): entity_id = 'alarm_control_panel.test' - alarm_control_panel.alarm_trigger(self.hass) + common.alarm_trigger(self.hass) self.hass.block_till_done() self.assertEqual(STATE_ALARM_PENDING, @@ -820,7 +821,7 @@ class TestAlarmControlPanelManual(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_trigger(self.hass, entity_id=entity_id) + common.alarm_trigger(self.hass, entity_id=entity_id) self.hass.block_till_done() self.assertEqual(STATE_ALARM_TRIGGERED, @@ -855,7 +856,7 @@ class TestAlarmControlPanelManual(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_trigger(self.hass, entity_id=entity_id) + common.alarm_trigger(self.hass, entity_id=entity_id) self.hass.block_till_done() self.assertEqual(STATE_ALARM_DISARMED, @@ -881,7 +882,7 @@ class TestAlarmControlPanelManual(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_trigger(self.hass, entity_id=entity_id) + common.alarm_trigger(self.hass, entity_id=entity_id) self.hass.block_till_done() self.assertEqual(STATE_ALARM_TRIGGERED, @@ -915,7 +916,7 @@ class TestAlarmControlPanelManual(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_trigger(self.hass, entity_id=entity_id) + common.alarm_trigger(self.hass, entity_id=entity_id) self.hass.block_till_done() self.assertEqual(STATE_ALARM_TRIGGERED, @@ -947,13 +948,13 @@ class TestAlarmControlPanelManual(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_arm_away(self.hass, CODE, entity_id) + common.alarm_arm_away(self.hass, CODE, entity_id) self.hass.block_till_done() self.assertEqual(STATE_ALARM_ARMED_AWAY, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_trigger(self.hass, entity_id=entity_id) + common.alarm_trigger(self.hass, entity_id=entity_id) self.hass.block_till_done() self.assertEqual(STATE_ALARM_TRIGGERED, @@ -985,13 +986,13 @@ class TestAlarmControlPanelManual(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_arm_away(self.hass, CODE, entity_id) + common.alarm_arm_away(self.hass, CODE, entity_id) self.hass.block_till_done() self.assertEqual(STATE_ALARM_ARMED_AWAY, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_trigger(self.hass, entity_id=entity_id) + common.alarm_trigger(self.hass, entity_id=entity_id) self.hass.block_till_done() self.assertEqual(STATE_ALARM_TRIGGERED, @@ -1006,7 +1007,7 @@ class TestAlarmControlPanelManual(unittest.TestCase): self.assertEqual(STATE_ALARM_ARMED_AWAY, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_trigger(self.hass, entity_id=entity_id) + common.alarm_trigger(self.hass, entity_id=entity_id) self.hass.block_till_done() self.assertEqual(STATE_ALARM_TRIGGERED, @@ -1037,13 +1038,13 @@ class TestAlarmControlPanelManual(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_trigger(self.hass) + common.alarm_trigger(self.hass) self.hass.block_till_done() self.assertEqual(STATE_ALARM_PENDING, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_disarm(self.hass, entity_id=entity_id) + common.alarm_disarm(self.hass, entity_id=entity_id) self.hass.block_till_done() self.assertEqual(STATE_ALARM_DISARMED, @@ -1075,13 +1076,13 @@ class TestAlarmControlPanelManual(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_trigger(self.hass) + common.alarm_trigger(self.hass) self.hass.block_till_done() self.assertEqual(STATE_ALARM_PENDING, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_disarm(self.hass, entity_id=entity_id) + common.alarm_disarm(self.hass, entity_id=entity_id) self.hass.block_till_done() self.assertEqual(STATE_ALARM_PENDING, @@ -1117,19 +1118,19 @@ class TestAlarmControlPanelManual(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_arm_home(self.hass, 'def') + common.alarm_arm_home(self.hass, 'def') self.hass.block_till_done() state = self.hass.states.get(entity_id) self.assertEqual(STATE_ALARM_ARMED_HOME, state.state) - alarm_control_panel.alarm_disarm(self.hass, 'def') + common.alarm_disarm(self.hass, 'def') self.hass.block_till_done() state = self.hass.states.get(entity_id) self.assertEqual(STATE_ALARM_ARMED_HOME, state.state) - alarm_control_panel.alarm_disarm(self.hass, 'abc') + common.alarm_disarm(self.hass, 'abc') self.hass.block_till_done() state = self.hass.states.get(entity_id) @@ -1152,7 +1153,7 @@ class TestAlarmControlPanelManual(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_arm_custom_bypass(self.hass, CODE) + common.alarm_arm_custom_bypass(self.hass, CODE) self.hass.block_till_done() self.assertEqual(STATE_ALARM_ARMED_CUSTOM_BYPASS, @@ -1175,7 +1176,7 @@ class TestAlarmControlPanelManual(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_arm_custom_bypass(self.hass, CODE, entity_id) + common.alarm_arm_custom_bypass(self.hass, CODE, entity_id) self.hass.block_till_done() self.assertEqual(STATE_ALARM_PENDING, @@ -1211,7 +1212,7 @@ class TestAlarmControlPanelManual(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_arm_custom_bypass(self.hass, CODE + '2') + common.alarm_arm_custom_bypass(self.hass, CODE + '2') self.hass.block_till_done() self.assertEqual(STATE_ALARM_DISARMED, @@ -1232,7 +1233,7 @@ class TestAlarmControlPanelManual(unittest.TestCase): entity_id = 'alarm_control_panel.test' - alarm_control_panel.alarm_arm_custom_bypass(self.hass) + common.alarm_arm_custom_bypass(self.hass) self.hass.block_till_done() self.assertEqual(STATE_ALARM_PENDING, @@ -1271,7 +1272,7 @@ class TestAlarmControlPanelManual(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_arm_away(self.hass, CODE) + common.alarm_arm_away(self.hass, CODE) self.hass.block_till_done() state = self.hass.states.get(entity_id) @@ -1281,7 +1282,7 @@ class TestAlarmControlPanelManual(unittest.TestCase): self.assertEqual(STATE_ALARM_ARMED_AWAY, state.attributes['post_pending_state']) - alarm_control_panel.alarm_trigger(self.hass, entity_id=entity_id) + common.alarm_trigger(self.hass, entity_id=entity_id) self.hass.block_till_done() state = self.hass.states.get(entity_id) @@ -1300,7 +1301,7 @@ class TestAlarmControlPanelManual(unittest.TestCase): state = self.hass.states.get(entity_id) self.assertEqual(STATE_ALARM_ARMED_AWAY, state.state) - alarm_control_panel.alarm_trigger(self.hass, entity_id=entity_id) + common.alarm_trigger(self.hass, entity_id=entity_id) self.hass.block_till_done() state = self.hass.states.get(entity_id) diff --git a/tests/components/alarm_control_panel/test_manual_mqtt.py b/tests/components/alarm_control_panel/test_manual_mqtt.py index 0158381b526..4e2ec6a9489 100644 --- a/tests/components/alarm_control_panel/test_manual_mqtt.py +++ b/tests/components/alarm_control_panel/test_manual_mqtt.py @@ -13,6 +13,7 @@ import homeassistant.util.dt as dt_util from tests.common import ( fire_time_changed, get_test_home_assistant, mock_mqtt_component, fire_mqtt_message, assert_setup_component) +from tests.components.alarm_control_panel import common CODE = 'HELLO_CODE' @@ -70,7 +71,7 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_arm_home(self.hass, CODE) + common.alarm_arm_home(self.hass, CODE) self.hass.block_till_done() self.assertEqual(STATE_ALARM_ARMED_HOME, @@ -95,7 +96,7 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_arm_home(self.hass, CODE, entity_id) + common.alarm_arm_home(self.hass, CODE, entity_id) self.hass.block_till_done() self.assertEqual(STATE_ALARM_PENDING, @@ -132,7 +133,7 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_arm_home(self.hass, CODE + '2') + common.alarm_arm_home(self.hass, CODE + '2') self.hass.block_till_done() self.assertEqual(STATE_ALARM_DISARMED, @@ -157,7 +158,7 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_arm_away(self.hass, CODE, entity_id) + common.alarm_arm_away(self.hass, CODE, entity_id) self.hass.block_till_done() self.assertEqual(STATE_ALARM_ARMED_AWAY, @@ -185,7 +186,7 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_arm_home(self.hass, 'abc') + common.alarm_arm_home(self.hass, 'abc') self.hass.block_till_done() state = self.hass.states.get(entity_id) @@ -210,7 +211,7 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_arm_away(self.hass, CODE) + common.alarm_arm_away(self.hass, CODE) self.hass.block_till_done() self.assertEqual(STATE_ALARM_PENDING, @@ -247,7 +248,7 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_arm_away(self.hass, CODE + '2') + common.alarm_arm_away(self.hass, CODE + '2') self.hass.block_till_done() self.assertEqual(STATE_ALARM_DISARMED, @@ -272,7 +273,7 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_arm_night(self.hass, CODE, entity_id) + common.alarm_arm_night(self.hass, CODE, entity_id) self.hass.block_till_done() self.assertEqual(STATE_ALARM_ARMED_NIGHT, @@ -297,7 +298,7 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_arm_night(self.hass, CODE) + common.alarm_arm_night(self.hass, CODE) self.hass.block_till_done() self.assertEqual(STATE_ALARM_PENDING, @@ -317,7 +318,7 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): self.hass.states.get(entity_id).state) # Do not go to the pending state when updating to the same state - alarm_control_panel.alarm_arm_night(self.hass, CODE, entity_id) + common.alarm_arm_night(self.hass, CODE, entity_id) self.hass.block_till_done() self.assertEqual(STATE_ALARM_ARMED_NIGHT, @@ -342,7 +343,7 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_arm_night(self.hass, CODE + '2') + common.alarm_arm_night(self.hass, CODE + '2') self.hass.block_till_done() self.assertEqual(STATE_ALARM_DISARMED, @@ -366,7 +367,7 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_trigger(self.hass, entity_id=entity_id) + common.alarm_trigger(self.hass, entity_id=entity_id) self.hass.block_till_done() self.assertEqual(STATE_ALARM_PENDING, @@ -401,13 +402,13 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_arm_away(self.hass, CODE) + common.alarm_arm_away(self.hass, CODE) self.hass.block_till_done() self.assertEqual(STATE_ALARM_ARMED_AWAY, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_trigger(self.hass, entity_id=entity_id) + common.alarm_trigger(self.hass, entity_id=entity_id) self.hass.block_till_done() state = self.hass.states.get(entity_id) @@ -443,7 +444,7 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_trigger(self.hass) + common.alarm_trigger(self.hass) self.hass.block_till_done() self.assertEqual(STATE_ALARM_DISARMED, @@ -468,7 +469,7 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_trigger(self.hass) + common.alarm_trigger(self.hass) self.hass.block_till_done() self.assertEqual(STATE_ALARM_DISARMED, @@ -493,7 +494,7 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_trigger(self.hass) + common.alarm_trigger(self.hass) self.hass.block_till_done() self.assertEqual(STATE_ALARM_PENDING, @@ -539,7 +540,7 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_trigger(self.hass, entity_id=entity_id) + common.alarm_trigger(self.hass, entity_id=entity_id) self.hass.block_till_done() self.assertEqual(STATE_ALARM_TRIGGERED, @@ -576,7 +577,7 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_trigger(self.hass, entity_id=entity_id) + common.alarm_trigger(self.hass, entity_id=entity_id) self.hass.block_till_done() self.assertEqual(STATE_ALARM_DISARMED, @@ -604,7 +605,7 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_trigger(self.hass, entity_id=entity_id) + common.alarm_trigger(self.hass, entity_id=entity_id) self.hass.block_till_done() self.assertEqual(STATE_ALARM_TRIGGERED, @@ -640,7 +641,7 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_trigger(self.hass, entity_id=entity_id) + common.alarm_trigger(self.hass, entity_id=entity_id) self.hass.block_till_done() self.assertEqual(STATE_ALARM_TRIGGERED, @@ -674,13 +675,13 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_arm_away(self.hass, CODE, entity_id) + common.alarm_arm_away(self.hass, CODE, entity_id) self.hass.block_till_done() self.assertEqual(STATE_ALARM_ARMED_AWAY, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_trigger(self.hass, entity_id=entity_id) + common.alarm_trigger(self.hass, entity_id=entity_id) self.hass.block_till_done() self.assertEqual(STATE_ALARM_TRIGGERED, @@ -695,7 +696,7 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): self.assertEqual(STATE_ALARM_ARMED_AWAY, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_trigger(self.hass, entity_id=entity_id) + common.alarm_trigger(self.hass, entity_id=entity_id) self.hass.block_till_done() self.assertEqual(STATE_ALARM_TRIGGERED, @@ -728,13 +729,13 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_trigger(self.hass) + common.alarm_trigger(self.hass) self.hass.block_till_done() self.assertEqual(STATE_ALARM_PENDING, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_disarm(self.hass, entity_id=entity_id) + common.alarm_disarm(self.hass, entity_id=entity_id) self.hass.block_till_done() self.assertEqual(STATE_ALARM_DISARMED, @@ -768,13 +769,13 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_trigger(self.hass) + common.alarm_trigger(self.hass) self.hass.block_till_done() self.assertEqual(STATE_ALARM_PENDING, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_disarm(self.hass, entity_id=entity_id) + common.alarm_disarm(self.hass, entity_id=entity_id) self.hass.block_till_done() self.assertEqual(STATE_ALARM_PENDING, @@ -812,13 +813,13 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_arm_away(self.hass, CODE) + common.alarm_arm_away(self.hass, CODE) self.hass.block_till_done() self.assertEqual(STATE_ALARM_ARMED_AWAY, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_trigger(self.hass, entity_id=entity_id) + common.alarm_trigger(self.hass, entity_id=entity_id) self.hass.block_till_done() state = self.hass.states.get(entity_id) @@ -858,13 +859,13 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_arm_away(self.hass, CODE) + common.alarm_arm_away(self.hass, CODE) self.hass.block_till_done() self.assertEqual(STATE_ALARM_ARMED_AWAY, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_trigger(self.hass, entity_id=entity_id) + common.alarm_trigger(self.hass, entity_id=entity_id) self.hass.block_till_done() state = self.hass.states.get(entity_id) @@ -904,13 +905,13 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_arm_away(self.hass, CODE) + common.alarm_arm_away(self.hass, CODE) self.hass.block_till_done() self.assertEqual(STATE_ALARM_ARMED_AWAY, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_trigger(self.hass, entity_id=entity_id) + common.alarm_trigger(self.hass, entity_id=entity_id) self.hass.block_till_done() state = self.hass.states.get(entity_id) @@ -962,13 +963,13 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_arm_away(self.hass, CODE) + common.alarm_arm_away(self.hass, CODE) self.hass.block_till_done() self.assertEqual(STATE_ALARM_ARMED_AWAY, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_trigger(self.hass, entity_id=entity_id) + common.alarm_trigger(self.hass, entity_id=entity_id) self.hass.block_till_done() state = self.hass.states.get(entity_id) @@ -1011,7 +1012,7 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): entity_id = 'alarm_control_panel.test' - alarm_control_panel.alarm_arm_home(self.hass) + common.alarm_arm_home(self.hass) self.hass.block_till_done() self.assertEqual(STATE_ALARM_PENDING, @@ -1043,7 +1044,7 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): entity_id = 'alarm_control_panel.test' - alarm_control_panel.alarm_arm_away(self.hass) + common.alarm_arm_away(self.hass) self.hass.block_till_done() self.assertEqual(STATE_ALARM_PENDING, @@ -1075,7 +1076,7 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): entity_id = 'alarm_control_panel.test' - alarm_control_panel.alarm_arm_night(self.hass) + common.alarm_arm_night(self.hass) self.hass.block_till_done() self.assertEqual(STATE_ALARM_PENDING, @@ -1109,7 +1110,7 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): entity_id = 'alarm_control_panel.test' - alarm_control_panel.alarm_trigger(self.hass) + common.alarm_trigger(self.hass) self.hass.block_till_done() self.assertEqual(STATE_ALARM_PENDING, @@ -1159,7 +1160,7 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_arm_away(self.hass, CODE) + common.alarm_arm_away(self.hass, CODE) self.hass.block_till_done() state = self.hass.states.get(entity_id) @@ -1169,7 +1170,7 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): self.assertEqual(STATE_ALARM_ARMED_AWAY, state.attributes['post_pending_state']) - alarm_control_panel.alarm_trigger(self.hass, entity_id=entity_id) + common.alarm_trigger(self.hass, entity_id=entity_id) self.hass.block_till_done() state = self.hass.states.get(entity_id) @@ -1188,7 +1189,7 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): state = self.hass.states.get(entity_id) self.assertEqual(STATE_ALARM_ARMED_AWAY, state.state) - alarm_control_panel.alarm_trigger(self.hass, entity_id=entity_id) + common.alarm_trigger(self.hass, entity_id=entity_id) self.hass.block_till_done() state = self.hass.states.get(entity_id) @@ -1230,19 +1231,19 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_arm_home(self.hass, 'def') + common.alarm_arm_home(self.hass, 'def') self.hass.block_till_done() state = self.hass.states.get(entity_id) self.assertEqual(STATE_ALARM_ARMED_HOME, state.state) - alarm_control_panel.alarm_disarm(self.hass, 'def') + common.alarm_disarm(self.hass, 'def') self.hass.block_till_done() state = self.hass.states.get(entity_id) self.assertEqual(STATE_ALARM_ARMED_HOME, state.state) - alarm_control_panel.alarm_disarm(self.hass, 'abc') + common.alarm_disarm(self.hass, 'abc') self.hass.block_till_done() state = self.hass.states.get(entity_id) @@ -1368,7 +1369,7 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_trigger(self.hass) + common.alarm_trigger(self.hass) self.hass.block_till_done() self.assertEqual(STATE_ALARM_PENDING, @@ -1401,7 +1402,7 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): self.mock_publish.async_publish.reset_mock() # Arm in home mode - alarm_control_panel.alarm_arm_home(self.hass) + common.alarm_arm_home(self.hass) self.hass.block_till_done() self.mock_publish.async_publish.assert_called_once_with( 'alarm/state', STATE_ALARM_PENDING, 0, True) @@ -1417,7 +1418,7 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): self.mock_publish.async_publish.reset_mock() # Arm in away mode - alarm_control_panel.alarm_arm_away(self.hass) + common.alarm_arm_away(self.hass) self.hass.block_till_done() self.mock_publish.async_publish.assert_called_once_with( 'alarm/state', STATE_ALARM_PENDING, 0, True) @@ -1433,7 +1434,7 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): self.mock_publish.async_publish.reset_mock() # Arm in night mode - alarm_control_panel.alarm_arm_night(self.hass) + common.alarm_arm_night(self.hass) self.hass.block_till_done() self.mock_publish.async_publish.assert_called_once_with( 'alarm/state', STATE_ALARM_PENDING, 0, True) @@ -1449,7 +1450,7 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): self.mock_publish.async_publish.reset_mock() # Disarm - alarm_control_panel.alarm_disarm(self.hass) + common.alarm_disarm(self.hass) self.hass.block_till_done() self.mock_publish.async_publish.assert_called_once_with( 'alarm/state', STATE_ALARM_DISARMED, 0, True) diff --git a/tests/components/alarm_control_panel/test_mqtt.py b/tests/components/alarm_control_panel/test_mqtt.py index 3a68b3cee44..b77767980a7 100644 --- a/tests/components/alarm_control_panel/test_mqtt.py +++ b/tests/components/alarm_control_panel/test_mqtt.py @@ -12,6 +12,7 @@ from homeassistant.components.mqtt.discovery import async_start from tests.common import ( mock_mqtt_component, async_fire_mqtt_message, fire_mqtt_message, get_test_home_assistant, assert_setup_component) +from tests.components.alarm_control_panel import common CODE = 'HELLO_CODE' @@ -105,7 +106,7 @@ class TestAlarmControlPanelMQTT(unittest.TestCase): } }) - alarm_control_panel.alarm_arm_home(self.hass) + common.alarm_arm_home(self.hass) self.hass.block_till_done() self.mock_publish.async_publish.assert_called_once_with( 'alarm/command', 'ARM_HOME', 0, False) @@ -123,7 +124,7 @@ class TestAlarmControlPanelMQTT(unittest.TestCase): }) call_count = self.mock_publish.call_count - alarm_control_panel.alarm_arm_home(self.hass, 'abcd') + common.alarm_arm_home(self.hass, 'abcd') self.hass.block_till_done() self.assertEqual(call_count, self.mock_publish.call_count) @@ -138,7 +139,7 @@ class TestAlarmControlPanelMQTT(unittest.TestCase): } }) - alarm_control_panel.alarm_arm_away(self.hass) + common.alarm_arm_away(self.hass) self.hass.block_till_done() self.mock_publish.async_publish.assert_called_once_with( 'alarm/command', 'ARM_AWAY', 0, False) @@ -156,7 +157,7 @@ class TestAlarmControlPanelMQTT(unittest.TestCase): }) call_count = self.mock_publish.call_count - alarm_control_panel.alarm_arm_away(self.hass, 'abcd') + common.alarm_arm_away(self.hass, 'abcd') self.hass.block_till_done() self.assertEqual(call_count, self.mock_publish.call_count) @@ -171,7 +172,7 @@ class TestAlarmControlPanelMQTT(unittest.TestCase): } }) - alarm_control_panel.alarm_disarm(self.hass) + common.alarm_disarm(self.hass) self.hass.block_till_done() self.mock_publish.async_publish.assert_called_once_with( 'alarm/command', 'DISARM', 0, False) @@ -189,7 +190,7 @@ class TestAlarmControlPanelMQTT(unittest.TestCase): }) call_count = self.mock_publish.call_count - alarm_control_panel.alarm_disarm(self.hass, 'abcd') + common.alarm_disarm(self.hass, 'abcd') self.hass.block_till_done() self.assertEqual(call_count, self.mock_publish.call_count) diff --git a/tests/components/automation/common.py b/tests/components/automation/common.py new file mode 100644 index 00000000000..4c8f91849aa --- /dev/null +++ b/tests/components/automation/common.py @@ -0,0 +1,53 @@ +"""Collection of helper methods. + +All containing methods are legacy helpers that should not be used by new +components. Instead call the service directly. +""" +from homeassistant.components.automation import DOMAIN, SERVICE_TRIGGER +from homeassistant.const import ( + ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE, + SERVICE_RELOAD) +from homeassistant.loader import bind_hass + + +@bind_hass +def turn_on(hass, entity_id=None): + """Turn on specified automation or all.""" + data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} + hass.services.call(DOMAIN, SERVICE_TURN_ON, data) + + +@bind_hass +def turn_off(hass, entity_id=None): + """Turn off specified automation or all.""" + data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} + hass.services.call(DOMAIN, SERVICE_TURN_OFF, data) + + +@bind_hass +def toggle(hass, entity_id=None): + """Toggle specified automation or all.""" + data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} + hass.services.call(DOMAIN, SERVICE_TOGGLE, data) + + +@bind_hass +def trigger(hass, entity_id=None): + """Trigger specified automation or all.""" + data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} + hass.services.call(DOMAIN, SERVICE_TRIGGER, data) + + +@bind_hass +def reload(hass): + """Reload the automation from config.""" + hass.services.call(DOMAIN, SERVICE_RELOAD) + + +@bind_hass +def async_reload(hass): + """Reload the automation from config. + + Returns a coroutine object. + """ + return hass.services.async_call(DOMAIN, SERVICE_RELOAD) diff --git a/tests/components/automation/test_event.py b/tests/components/automation/test_event.py index 6e16c03f2dc..09d237013b0 100644 --- a/tests/components/automation/test_event.py +++ b/tests/components/automation/test_event.py @@ -6,6 +6,7 @@ from homeassistant.setup import setup_component import homeassistant.components.automation as automation from tests.common import get_test_home_assistant, mock_component +from tests.components.automation import common # pylint: disable=invalid-name @@ -50,7 +51,7 @@ class TestAutomationEvent(unittest.TestCase): self.assertEqual(1, len(self.calls)) assert self.calls[0].context is context - automation.turn_off(self.hass) + common.turn_off(self.hass) self.hass.block_till_done() self.hass.bus.fire('test_event') @@ -75,7 +76,7 @@ class TestAutomationEvent(unittest.TestCase): self.hass.block_till_done() self.assertEqual(1, len(self.calls)) - automation.turn_off(self.hass) + common.turn_off(self.hass) self.hass.block_till_done() self.hass.bus.fire('test_event') diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py index c3bd6c224af..3bcbc7da04f 100644 --- a/tests/components/automation/test_init.py +++ b/tests/components/automation/test_init.py @@ -15,6 +15,7 @@ import homeassistant.util.dt as dt_util from tests.common import ( assert_setup_component, get_test_home_assistant, fire_time_changed, mock_service, async_mock_service, mock_restore_cache) +from tests.components.automation import common # pylint: disable=invalid-name @@ -363,7 +364,7 @@ class TestAutomation(unittest.TestCase): self.hass.block_till_done() assert len(self.calls) == 1 - automation.turn_off(self.hass, entity_id) + common.turn_off(self.hass, entity_id) self.hass.block_till_done() assert not automation.is_on(self.hass, entity_id) @@ -371,7 +372,7 @@ class TestAutomation(unittest.TestCase): self.hass.block_till_done() assert len(self.calls) == 1 - automation.toggle(self.hass, entity_id) + common.toggle(self.hass, entity_id) self.hass.block_till_done() assert automation.is_on(self.hass, entity_id) @@ -379,17 +380,17 @@ class TestAutomation(unittest.TestCase): self.hass.block_till_done() assert len(self.calls) == 2 - automation.trigger(self.hass, entity_id) + common.trigger(self.hass, entity_id) self.hass.block_till_done() assert len(self.calls) == 3 - automation.turn_off(self.hass, entity_id) + common.turn_off(self.hass, entity_id) self.hass.block_till_done() - automation.trigger(self.hass, entity_id) + common.trigger(self.hass, entity_id) self.hass.block_till_done() assert len(self.calls) == 4 - automation.turn_on(self.hass, entity_id) + common.turn_on(self.hass, entity_id) self.hass.block_till_done() assert automation.is_on(self.hass, entity_id) @@ -439,7 +440,7 @@ class TestAutomation(unittest.TestCase): }}): with patch('homeassistant.config.find_config_file', return_value=''): - automation.reload(self.hass) + common.reload(self.hass) self.hass.block_till_done() # De-flake ?! self.hass.block_till_done() @@ -489,7 +490,7 @@ class TestAutomation(unittest.TestCase): return_value={automation.DOMAIN: 'not valid'}): with patch('homeassistant.config.find_config_file', return_value=''): - automation.reload(self.hass) + common.reload(self.hass) self.hass.block_till_done() assert self.hass.states.get('automation.hello') is None @@ -527,7 +528,7 @@ class TestAutomation(unittest.TestCase): side_effect=HomeAssistantError('bla')): with patch('homeassistant.config.find_config_file', return_value=''): - automation.reload(self.hass) + common.reload(self.hass) self.hass.block_till_done() assert self.hass.states.get('automation.hello') is not None diff --git a/tests/components/automation/test_litejet.py b/tests/components/automation/test_litejet.py index ca6f7796cfc..3d88174708b 100644 --- a/tests/components/automation/test_litejet.py +++ b/tests/components/automation/test_litejet.py @@ -7,9 +7,10 @@ from datetime import timedelta from homeassistant import setup import homeassistant.util.dt as dt_util from homeassistant.components import litejet -from tests.common import (fire_time_changed, get_test_home_assistant) import homeassistant.components.automation as automation +from tests.common import (fire_time_changed, get_test_home_assistant) + _LOGGER = logging.getLogger(__name__) ENTITY_SWITCH = 'switch.mock_switch_1' diff --git a/tests/components/automation/test_mqtt.py b/tests/components/automation/test_mqtt.py index 8ec5351af94..29a53467c4f 100644 --- a/tests/components/automation/test_mqtt.py +++ b/tests/components/automation/test_mqtt.py @@ -7,6 +7,7 @@ import homeassistant.components.automation as automation from tests.common import ( mock_mqtt_component, fire_mqtt_message, get_test_home_assistant, mock_component) +from tests.components.automation import common # pylint: disable=invalid-name @@ -56,7 +57,7 @@ class TestAutomationMQTT(unittest.TestCase): self.assertEqual('mqtt - test-topic - { "hello": "world" } - world', self.calls[0].data['some']) - automation.turn_off(self.hass) + common.turn_off(self.hass) self.hass.block_till_done() fire_mqtt_message(self.hass, 'test-topic', 'test_payload') self.hass.block_till_done() diff --git a/tests/components/automation/test_numeric_state.py b/tests/components/automation/test_numeric_state.py index af95bc0ff02..183d1f4a5f9 100644 --- a/tests/components/automation/test_numeric_state.py +++ b/tests/components/automation/test_numeric_state.py @@ -11,6 +11,7 @@ import homeassistant.util.dt as dt_util from tests.common import ( get_test_home_assistant, mock_component, fire_time_changed, assert_setup_component) +from tests.components.automation import common # pylint: disable=invalid-name @@ -57,7 +58,7 @@ class TestAutomationNumericState(unittest.TestCase): # Set above 12 so the automation will fire again self.hass.states.set('test.entity', 12) - automation.turn_off(self.hass) + common.turn_off(self.hass) self.hass.block_till_done() self.hass.states.set('test.entity', 9) self.hass.block_till_done() @@ -775,7 +776,7 @@ class TestAutomationNumericState(unittest.TestCase): self.hass.states.set('test.entity_1', 9) self.hass.states.set('test.entity_2', 9) self.hass.block_till_done() - automation.turn_off(self.hass) + common.turn_off(self.hass) self.hass.block_till_done() fire_time_changed(self.hass, dt_util.utcnow() + timedelta(seconds=10)) diff --git a/tests/components/automation/test_state.py b/tests/components/automation/test_state.py index 274980fabc0..15c6353b234 100644 --- a/tests/components/automation/test_state.py +++ b/tests/components/automation/test_state.py @@ -12,6 +12,7 @@ import homeassistant.components.automation as automation from tests.common import ( fire_time_changed, get_test_home_assistant, assert_setup_component, mock_component) +from tests.components.automation import common # pylint: disable=invalid-name @@ -68,7 +69,7 @@ class TestAutomationState(unittest.TestCase): 'state - test.entity - hello - world - None', self.calls[0].data['some']) - automation.turn_off(self.hass) + common.turn_off(self.hass) self.hass.block_till_done() self.hass.states.set('test.entity', 'planet') self.hass.block_till_done() @@ -370,7 +371,7 @@ class TestAutomationState(unittest.TestCase): self.hass.states.set('test.entity_1', 'world') self.hass.states.set('test.entity_2', 'world') self.hass.block_till_done() - automation.turn_off(self.hass) + common.turn_off(self.hass) self.hass.block_till_done() fire_time_changed(self.hass, dt_util.utcnow() + timedelta(seconds=10)) diff --git a/tests/components/automation/test_sun.py b/tests/components/automation/test_sun.py index 4556b7cbe45..ad8709fdf36 100644 --- a/tests/components/automation/test_sun.py +++ b/tests/components/automation/test_sun.py @@ -12,6 +12,7 @@ import homeassistant.util.dt as dt_util from tests.common import ( fire_time_changed, get_test_home_assistant, mock_component) +from tests.components.automation import common # pylint: disable=invalid-name @@ -57,7 +58,7 @@ class TestAutomationSun(unittest.TestCase): } }) - automation.turn_off(self.hass) + common.turn_off(self.hass) self.hass.block_till_done() fire_time_changed(self.hass, trigger_time) @@ -66,7 +67,7 @@ class TestAutomationSun(unittest.TestCase): with patch('homeassistant.util.dt.utcnow', return_value=now): - automation.turn_on(self.hass) + common.turn_on(self.hass) self.hass.block_till_done() fire_time_changed(self.hass, trigger_time) diff --git a/tests/components/automation/test_template.py b/tests/components/automation/test_template.py index e9c763ccc73..4fec0e707a9 100644 --- a/tests/components/automation/test_template.py +++ b/tests/components/automation/test_template.py @@ -7,6 +7,7 @@ import homeassistant.components.automation as automation from tests.common import ( get_test_home_assistant, assert_setup_component, mock_component) +from tests.components.automation import common # pylint: disable=invalid-name @@ -49,7 +50,7 @@ class TestAutomationTemplate(unittest.TestCase): self.hass.block_till_done() self.assertEqual(1, len(self.calls)) - automation.turn_off(self.hass) + common.turn_off(self.hass) self.hass.block_till_done() self.hass.states.set('test.entity', 'planet') diff --git a/tests/components/automation/test_time.py b/tests/components/automation/test_time.py index 5f928cf92a0..dcb723d725e 100644 --- a/tests/components/automation/test_time.py +++ b/tests/components/automation/test_time.py @@ -11,6 +11,7 @@ import homeassistant.components.automation as automation from tests.common import ( fire_time_changed, get_test_home_assistant, assert_setup_component, mock_component) +from tests.components.automation import common # pylint: disable=invalid-name @@ -52,7 +53,7 @@ class TestAutomationTime(unittest.TestCase): self.hass.block_till_done() self.assertEqual(1, len(self.calls)) - automation.turn_off(self.hass) + common.turn_off(self.hass) self.hass.block_till_done() fire_time_changed(self.hass, dt_util.utcnow().replace(hour=0)) diff --git a/tests/components/automation/test_zone.py b/tests/components/automation/test_zone.py index d146278a997..795f55a3e0b 100644 --- a/tests/components/automation/test_zone.py +++ b/tests/components/automation/test_zone.py @@ -6,6 +6,7 @@ from homeassistant.setup import setup_component from homeassistant.components import automation, zone from tests.common import get_test_home_assistant, mock_component +from tests.components.automation import common # pylint: disable=invalid-name @@ -87,7 +88,7 @@ class TestAutomationZone(unittest.TestCase): }) self.hass.block_till_done() - automation.turn_off(self.hass) + common.turn_off(self.hass) self.hass.block_till_done() self.hass.states.set('test.entity', 'hello', { diff --git a/tests/components/camera/common.py b/tests/components/camera/common.py new file mode 100644 index 00000000000..71193f1a99d --- /dev/null +++ b/tests/components/camera/common.py @@ -0,0 +1,68 @@ +"""Collection of helper methods. + +All containing methods are legacy helpers that should not be used by new +components. Instead call the service directly. +""" +from homeassistant.components.camera import ( + ATTR_FILENAME, DOMAIN, SERVICE_DISABLE_MOTION, SERVICE_ENABLE_MOTION, + SERVICE_SNAPSHOT) +from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, \ + SERVICE_TURN_ON +from homeassistant.core import callback +from homeassistant.loader import bind_hass + + +@bind_hass +def turn_off(hass, entity_id=None): + """Turn off camera.""" + hass.add_job(async_turn_off, hass, entity_id) + + +@bind_hass +async def async_turn_off(hass, entity_id=None): + """Turn off camera.""" + data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} + await hass.services.async_call(DOMAIN, SERVICE_TURN_OFF, data) + + +@bind_hass +def turn_on(hass, entity_id=None): + """Turn on camera.""" + hass.add_job(async_turn_on, hass, entity_id) + + +@bind_hass +async def async_turn_on(hass, entity_id=None): + """Turn on camera, and set operation mode.""" + data = {} + if entity_id is not None: + data[ATTR_ENTITY_ID] = entity_id + + await hass.services.async_call(DOMAIN, SERVICE_TURN_ON, data) + + +@bind_hass +def enable_motion_detection(hass, entity_id=None): + """Enable Motion Detection.""" + data = {ATTR_ENTITY_ID: entity_id} if entity_id else None + hass.async_add_job(hass.services.async_call( + DOMAIN, SERVICE_ENABLE_MOTION, data)) + + +@bind_hass +def disable_motion_detection(hass, entity_id=None): + """Disable Motion Detection.""" + data = {ATTR_ENTITY_ID: entity_id} if entity_id else None + hass.async_add_job(hass.services.async_call( + DOMAIN, SERVICE_DISABLE_MOTION, data)) + + +@bind_hass +@callback +def async_snapshot(hass, filename, entity_id=None): + """Make a snapshot from a camera.""" + data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} + data[ATTR_FILENAME] = filename + + hass.async_add_job(hass.services.async_call( + DOMAIN, SERVICE_SNAPSHOT, data)) diff --git a/tests/components/camera/test_demo.py b/tests/components/camera/test_demo.py index 63c70ddc6ca..f6e2513380c 100644 --- a/tests/components/camera/test_demo.py +++ b/tests/components/camera/test_demo.py @@ -8,6 +8,8 @@ from homeassistant.components.camera import STATE_STREAMING, STATE_IDLE from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import async_setup_component +from tests.components.camera import common + @pytest.fixture def demo_camera(hass): @@ -37,12 +39,12 @@ async def test_init_state_is_streaming(hass, demo_camera): async def test_turn_on_state_back_to_streaming(hass, demo_camera): """After turn on state back to streaming.""" assert demo_camera.state == STATE_STREAMING - await camera.async_turn_off(hass, demo_camera.entity_id) + await common.async_turn_off(hass, demo_camera.entity_id) await hass.async_block_till_done() assert demo_camera.state == STATE_IDLE - await camera.async_turn_on(hass, demo_camera.entity_id) + await common.async_turn_on(hass, demo_camera.entity_id) await hass.async_block_till_done() assert demo_camera.state == STATE_STREAMING @@ -50,7 +52,7 @@ async def test_turn_on_state_back_to_streaming(hass, demo_camera): async def test_turn_off_image(hass, demo_camera): """After turn off, Demo camera raise error.""" - await camera.async_turn_off(hass, demo_camera.entity_id) + await common.async_turn_off(hass, demo_camera.entity_id) await hass.async_block_till_done() with pytest.raises(HomeAssistantError) as error: @@ -61,7 +63,7 @@ async def test_turn_off_image(hass, demo_camera): async def test_turn_off_invalid_camera(hass, demo_camera): """Turn off non-exist camera should quietly fail.""" assert demo_camera.state == STATE_STREAMING - await camera.async_turn_off(hass, 'camera.invalid_camera') + await common.async_turn_off(hass, 'camera.invalid_camera') await hass.async_block_till_done() assert demo_camera.state == STATE_STREAMING @@ -81,7 +83,7 @@ async def test_motion_detection(hass): assert not state.attributes.get('motion_detection') # Call service to turn on motion detection - camera.enable_motion_detection(hass, 'camera.demo_camera') + common.enable_motion_detection(hass, 'camera.demo_camera') await hass.async_block_till_done() # Check if state has been updated. diff --git a/tests/components/camera/test_init.py b/tests/components/camera/test_init.py index 053fa6d29dc..2129e39a43c 100644 --- a/tests/components/camera/test_init.py +++ b/tests/components/camera/test_init.py @@ -14,6 +14,7 @@ from homeassistant.util.async_ import run_coroutine_threadsafe from tests.common import ( get_test_home_assistant, get_test_instance_port, assert_setup_component, mock_coro) +from tests.components.camera import common @pytest.fixture @@ -126,7 +127,7 @@ def test_snapshot_service(hass, mock_camera): with patch('homeassistant.components.camera.open', mopen, create=True), \ patch.object(hass.config, 'is_allowed_path', return_value=True): - hass.components.camera.async_snapshot('/tmp/bla') + common.async_snapshot(hass, '/tmp/bla') yield from hass.async_block_till_done() mock_write = mopen().write diff --git a/tests/components/climate/common.py b/tests/components/climate/common.py new file mode 100644 index 00000000000..4ac6f553091 --- /dev/null +++ b/tests/components/climate/common.py @@ -0,0 +1,115 @@ +"""Collection of helper methods. + +All containing methods are legacy helpers that should not be used by new +components. Instead call the service directly. +""" +from homeassistant.components.climate import ( + _LOGGER, ATTR_AUX_HEAT, ATTR_AWAY_MODE, ATTR_FAN_MODE, ATTR_HOLD_MODE, + ATTR_HUMIDITY, ATTR_OPERATION_MODE, ATTR_SWING_MODE, ATTR_TARGET_TEMP_HIGH, + ATTR_TARGET_TEMP_LOW, DOMAIN, SERVICE_SET_AWAY_MODE, SERVICE_SET_HOLD_MODE, + SERVICE_SET_AUX_HEAT, SERVICE_SET_TEMPERATURE, SERVICE_SET_HUMIDITY, + SERVICE_SET_FAN_MODE, SERVICE_SET_OPERATION_MODE, SERVICE_SET_SWING_MODE) +from homeassistant.const import ( + ATTR_ENTITY_ID, ATTR_TEMPERATURE) +from homeassistant.loader import bind_hass + + +@bind_hass +def set_away_mode(hass, away_mode, entity_id=None): + """Turn all or specified climate devices away mode on.""" + data = { + ATTR_AWAY_MODE: away_mode + } + + if entity_id: + data[ATTR_ENTITY_ID] = entity_id + + hass.services.call(DOMAIN, SERVICE_SET_AWAY_MODE, data) + + +@bind_hass +def set_hold_mode(hass, hold_mode, entity_id=None): + """Set new hold mode.""" + data = { + ATTR_HOLD_MODE: hold_mode + } + + if entity_id: + data[ATTR_ENTITY_ID] = entity_id + + hass.services.call(DOMAIN, SERVICE_SET_HOLD_MODE, data) + + +@bind_hass +def set_aux_heat(hass, aux_heat, entity_id=None): + """Turn all or specified climate devices auxiliary heater on.""" + data = { + ATTR_AUX_HEAT: aux_heat + } + + if entity_id: + data[ATTR_ENTITY_ID] = entity_id + + hass.services.call(DOMAIN, SERVICE_SET_AUX_HEAT, data) + + +@bind_hass +def set_temperature(hass, temperature=None, entity_id=None, + target_temp_high=None, target_temp_low=None, + operation_mode=None): + """Set new target temperature.""" + kwargs = { + key: value for key, value in [ + (ATTR_TEMPERATURE, temperature), + (ATTR_TARGET_TEMP_HIGH, target_temp_high), + (ATTR_TARGET_TEMP_LOW, target_temp_low), + (ATTR_ENTITY_ID, entity_id), + (ATTR_OPERATION_MODE, operation_mode) + ] if value is not None + } + _LOGGER.debug("set_temperature start data=%s", kwargs) + hass.services.call(DOMAIN, SERVICE_SET_TEMPERATURE, kwargs) + + +@bind_hass +def set_humidity(hass, humidity, entity_id=None): + """Set new target humidity.""" + data = {ATTR_HUMIDITY: humidity} + + if entity_id is not None: + data[ATTR_ENTITY_ID] = entity_id + + hass.services.call(DOMAIN, SERVICE_SET_HUMIDITY, data) + + +@bind_hass +def set_fan_mode(hass, fan, entity_id=None): + """Set all or specified climate devices fan mode on.""" + data = {ATTR_FAN_MODE: fan} + + if entity_id: + data[ATTR_ENTITY_ID] = entity_id + + hass.services.call(DOMAIN, SERVICE_SET_FAN_MODE, data) + + +@bind_hass +def set_operation_mode(hass, operation_mode, entity_id=None): + """Set new target operation mode.""" + data = {ATTR_OPERATION_MODE: operation_mode} + + if entity_id is not None: + data[ATTR_ENTITY_ID] = entity_id + + hass.services.call(DOMAIN, SERVICE_SET_OPERATION_MODE, data) + + +@bind_hass +def set_swing_mode(hass, swing_mode, entity_id=None): + """Set new target swing mode.""" + data = {ATTR_SWING_MODE: swing_mode} + + if entity_id is not None: + data[ATTR_ENTITY_ID] = entity_id + + hass.services.call(DOMAIN, SERVICE_SET_SWING_MODE, data) diff --git a/tests/components/climate/test_demo.py b/tests/components/climate/test_demo.py index 0cd6d288536..4990a8a6998 100644 --- a/tests/components/climate/test_demo.py +++ b/tests/components/climate/test_demo.py @@ -8,6 +8,7 @@ from homeassistant.setup import setup_component from homeassistant.components import climate from tests.common import get_test_home_assistant +from tests.components.climate import common ENTITY_CLIMATE = 'climate.hvac' @@ -56,7 +57,7 @@ class TestDemoClimate(unittest.TestCase): """Test setting the target temperature without required attribute.""" state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual(21, state.attributes.get('temperature')) - climate.set_temperature(self.hass, None, ENTITY_CLIMATE) + common.set_temperature(self.hass, None, ENTITY_CLIMATE) self.hass.block_till_done() self.assertEqual(21, state.attributes.get('temperature')) @@ -64,7 +65,7 @@ class TestDemoClimate(unittest.TestCase): """Test the setting of the target temperature.""" state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual(21, state.attributes.get('temperature')) - climate.set_temperature(self.hass, 30, ENTITY_CLIMATE) + common.set_temperature(self.hass, 30, ENTITY_CLIMATE) self.hass.block_till_done() state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual(30.0, state.attributes.get('temperature')) @@ -73,7 +74,7 @@ class TestDemoClimate(unittest.TestCase): """Test the setting of the target temperature.""" state = self.hass.states.get(ENTITY_HEATPUMP) self.assertEqual(20, state.attributes.get('temperature')) - climate.set_temperature(self.hass, 21, ENTITY_HEATPUMP) + common.set_temperature(self.hass, 21, ENTITY_HEATPUMP) self.hass.block_till_done() state = self.hass.states.get(ENTITY_HEATPUMP) self.assertEqual(21.0, state.attributes.get('temperature')) @@ -84,8 +85,8 @@ class TestDemoClimate(unittest.TestCase): self.assertEqual(None, state.attributes.get('temperature')) self.assertEqual(21.0, state.attributes.get('target_temp_low')) self.assertEqual(24.0, state.attributes.get('target_temp_high')) - climate.set_temperature(self.hass, target_temp_high=25, - target_temp_low=20, entity_id=ENTITY_ECOBEE) + common.set_temperature(self.hass, target_temp_high=25, + target_temp_low=20, entity_id=ENTITY_ECOBEE) self.hass.block_till_done() state = self.hass.states.get(ENTITY_ECOBEE) self.assertEqual(None, state.attributes.get('temperature')) @@ -98,9 +99,9 @@ class TestDemoClimate(unittest.TestCase): self.assertEqual(None, state.attributes.get('temperature')) self.assertEqual(21.0, state.attributes.get('target_temp_low')) self.assertEqual(24.0, state.attributes.get('target_temp_high')) - climate.set_temperature(self.hass, temperature=None, - entity_id=ENTITY_ECOBEE, target_temp_low=None, - target_temp_high=None) + common.set_temperature(self.hass, temperature=None, + entity_id=ENTITY_ECOBEE, target_temp_low=None, + target_temp_high=None) self.hass.block_till_done() state = self.hass.states.get(ENTITY_ECOBEE) self.assertEqual(None, state.attributes.get('temperature')) @@ -111,7 +112,7 @@ class TestDemoClimate(unittest.TestCase): """Test setting the target humidity without required attribute.""" state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual(67, state.attributes.get('humidity')) - climate.set_humidity(self.hass, None, ENTITY_CLIMATE) + common.set_humidity(self.hass, None, ENTITY_CLIMATE) self.hass.block_till_done() state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual(67, state.attributes.get('humidity')) @@ -120,7 +121,7 @@ class TestDemoClimate(unittest.TestCase): """Test the setting of the target humidity.""" state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual(67, state.attributes.get('humidity')) - climate.set_humidity(self.hass, 64, ENTITY_CLIMATE) + common.set_humidity(self.hass, 64, ENTITY_CLIMATE) self.hass.block_till_done() state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual(64.0, state.attributes.get('humidity')) @@ -129,7 +130,7 @@ class TestDemoClimate(unittest.TestCase): """Test setting fan mode without required attribute.""" state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual("On High", state.attributes.get('fan_mode')) - climate.set_fan_mode(self.hass, None, ENTITY_CLIMATE) + common.set_fan_mode(self.hass, None, ENTITY_CLIMATE) self.hass.block_till_done() state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual("On High", state.attributes.get('fan_mode')) @@ -138,7 +139,7 @@ class TestDemoClimate(unittest.TestCase): """Test setting of new fan mode.""" state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual("On High", state.attributes.get('fan_mode')) - climate.set_fan_mode(self.hass, "On Low", ENTITY_CLIMATE) + common.set_fan_mode(self.hass, "On Low", ENTITY_CLIMATE) self.hass.block_till_done() state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual("On Low", state.attributes.get('fan_mode')) @@ -147,7 +148,7 @@ class TestDemoClimate(unittest.TestCase): """Test setting swing mode without required attribute.""" state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual("Off", state.attributes.get('swing_mode')) - climate.set_swing_mode(self.hass, None, ENTITY_CLIMATE) + common.set_swing_mode(self.hass, None, ENTITY_CLIMATE) self.hass.block_till_done() state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual("Off", state.attributes.get('swing_mode')) @@ -156,7 +157,7 @@ class TestDemoClimate(unittest.TestCase): """Test setting of new swing mode.""" state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual("Off", state.attributes.get('swing_mode')) - climate.set_swing_mode(self.hass, "Auto", ENTITY_CLIMATE) + common.set_swing_mode(self.hass, "Auto", ENTITY_CLIMATE) self.hass.block_till_done() state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual("Auto", state.attributes.get('swing_mode')) @@ -169,7 +170,7 @@ class TestDemoClimate(unittest.TestCase): state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual("cool", state.attributes.get('operation_mode')) self.assertEqual("cool", state.state) - climate.set_operation_mode(self.hass, None, ENTITY_CLIMATE) + common.set_operation_mode(self.hass, None, ENTITY_CLIMATE) self.hass.block_till_done() state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual("cool", state.attributes.get('operation_mode')) @@ -180,7 +181,7 @@ class TestDemoClimate(unittest.TestCase): state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual("cool", state.attributes.get('operation_mode')) self.assertEqual("cool", state.state) - climate.set_operation_mode(self.hass, "heat", ENTITY_CLIMATE) + common.set_operation_mode(self.hass, "heat", ENTITY_CLIMATE) self.hass.block_till_done() state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual("heat", state.attributes.get('operation_mode')) @@ -190,41 +191,41 @@ class TestDemoClimate(unittest.TestCase): """Test setting the away mode without required attribute.""" state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual('on', state.attributes.get('away_mode')) - climate.set_away_mode(self.hass, None, ENTITY_CLIMATE) + common.set_away_mode(self.hass, None, ENTITY_CLIMATE) self.hass.block_till_done() self.assertEqual('on', state.attributes.get('away_mode')) def test_set_away_mode_on(self): """Test setting the away mode on/true.""" - climate.set_away_mode(self.hass, True, ENTITY_CLIMATE) + common.set_away_mode(self.hass, True, ENTITY_CLIMATE) self.hass.block_till_done() state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual('on', state.attributes.get('away_mode')) def test_set_away_mode_off(self): """Test setting the away mode off/false.""" - climate.set_away_mode(self.hass, False, ENTITY_CLIMATE) + common.set_away_mode(self.hass, False, ENTITY_CLIMATE) self.hass.block_till_done() state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual('off', state.attributes.get('away_mode')) def test_set_hold_mode_home(self): """Test setting the hold mode home.""" - climate.set_hold_mode(self.hass, 'home', ENTITY_ECOBEE) + common.set_hold_mode(self.hass, 'home', ENTITY_ECOBEE) self.hass.block_till_done() state = self.hass.states.get(ENTITY_ECOBEE) self.assertEqual('home', state.attributes.get('hold_mode')) def test_set_hold_mode_away(self): """Test setting the hold mode away.""" - climate.set_hold_mode(self.hass, 'away', ENTITY_ECOBEE) + common.set_hold_mode(self.hass, 'away', ENTITY_ECOBEE) self.hass.block_till_done() state = self.hass.states.get(ENTITY_ECOBEE) self.assertEqual('away', state.attributes.get('hold_mode')) def test_set_hold_mode_none(self): """Test setting the hold mode off/false.""" - climate.set_hold_mode(self.hass, 'off', ENTITY_ECOBEE) + common.set_hold_mode(self.hass, 'off', ENTITY_ECOBEE) self.hass.block_till_done() state = self.hass.states.get(ENTITY_ECOBEE) self.assertEqual('off', state.attributes.get('hold_mode')) @@ -233,20 +234,20 @@ class TestDemoClimate(unittest.TestCase): """Test setting the auxiliary heater without required attribute.""" state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual('off', state.attributes.get('aux_heat')) - climate.set_aux_heat(self.hass, None, ENTITY_CLIMATE) + common.set_aux_heat(self.hass, None, ENTITY_CLIMATE) self.hass.block_till_done() self.assertEqual('off', state.attributes.get('aux_heat')) def test_set_aux_heat_on(self): """Test setting the axillary heater on/true.""" - climate.set_aux_heat(self.hass, True, ENTITY_CLIMATE) + common.set_aux_heat(self.hass, True, ENTITY_CLIMATE) self.hass.block_till_done() state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual('on', state.attributes.get('aux_heat')) def test_set_aux_heat_off(self): """Test setting the auxiliary heater off/false.""" - climate.set_aux_heat(self.hass, False, ENTITY_CLIMATE) + common.set_aux_heat(self.hass, False, ENTITY_CLIMATE) self.hass.block_till_done() state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual('off', state.attributes.get('aux_heat')) diff --git a/tests/components/climate/test_generic_thermostat.py b/tests/components/climate/test_generic_thermostat.py index c4e07705230..47ec621aeb5 100644 --- a/tests/components/climate/test_generic_thermostat.py +++ b/tests/components/climate/test_generic_thermostat.py @@ -25,6 +25,7 @@ from homeassistant.components.climate import STATE_HEAT, STATE_COOL import homeassistant.components as comps from tests.common import (assert_setup_component, get_test_home_assistant, mock_restore_cache) +from tests.components.climate import common ENTITY = 'climate.test' @@ -108,7 +109,7 @@ class TestGenericThermostatHeaterSwitching(unittest.TestCase): self._setup_sensor(18) self.hass.block_till_done() - climate.set_temperature(self.hass, 23) + common.set_temperature(self.hass, 23) self.hass.block_till_done() self.assertEqual(STATE_ON, @@ -135,7 +136,7 @@ class TestGenericThermostatHeaterSwitching(unittest.TestCase): self._setup_sensor(18) self.hass.block_till_done() - climate.set_temperature(self.hass, 23) + common.set_temperature(self.hass, 23) self.hass.block_till_done() self.assertEqual(STATE_ON, @@ -186,20 +187,20 @@ class TestClimateGenericThermostat(unittest.TestCase): def test_set_target_temp(self): """Test the setting of the target temperature.""" - climate.set_temperature(self.hass, 30) + common.set_temperature(self.hass, 30) self.hass.block_till_done() state = self.hass.states.get(ENTITY) self.assertEqual(30.0, state.attributes.get('temperature')) - climate.set_temperature(self.hass, None) + common.set_temperature(self.hass, None) self.hass.block_till_done() state = self.hass.states.get(ENTITY) self.assertEqual(30.0, state.attributes.get('temperature')) def test_set_away_mode(self): """Test the setting away mode.""" - climate.set_temperature(self.hass, 23) + common.set_temperature(self.hass, 23) self.hass.block_till_done() - climate.set_away_mode(self.hass, True) + common.set_away_mode(self.hass, True) self.hass.block_till_done() state = self.hass.states.get(ENTITY) self.assertEqual(16, state.attributes.get('temperature')) @@ -209,13 +210,13 @@ class TestClimateGenericThermostat(unittest.TestCase): Verify original temperature is restored. """ - climate.set_temperature(self.hass, 23) + common.set_temperature(self.hass, 23) self.hass.block_till_done() - climate.set_away_mode(self.hass, True) + common.set_away_mode(self.hass, True) self.hass.block_till_done() state = self.hass.states.get(ENTITY) self.assertEqual(16, state.attributes.get('temperature')) - climate.set_away_mode(self.hass, False) + common.set_away_mode(self.hass, False) self.hass.block_till_done() state = self.hass.states.get(ENTITY) self.assertEqual(23, state.attributes.get('temperature')) @@ -236,7 +237,7 @@ class TestClimateGenericThermostat(unittest.TestCase): self._setup_switch(False) self._setup_sensor(25) self.hass.block_till_done() - climate.set_temperature(self.hass, 30) + common.set_temperature(self.hass, 30) self.hass.block_till_done() self.assertEqual(1, len(self.calls)) call = self.calls[0] @@ -249,7 +250,7 @@ class TestClimateGenericThermostat(unittest.TestCase): self._setup_switch(True) self._setup_sensor(30) self.hass.block_till_done() - climate.set_temperature(self.hass, 25) + common.set_temperature(self.hass, 25) self.hass.block_till_done() self.assertEqual(2, len(self.calls)) call = self.calls[0] @@ -260,7 +261,7 @@ class TestClimateGenericThermostat(unittest.TestCase): def test_temp_change_heater_on_within_tolerance(self): """Test if temperature change doesn't turn on within tolerance.""" self._setup_switch(False) - climate.set_temperature(self.hass, 30) + common.set_temperature(self.hass, 30) self.hass.block_till_done() self._setup_sensor(29) self.hass.block_till_done() @@ -269,7 +270,7 @@ class TestClimateGenericThermostat(unittest.TestCase): def test_temp_change_heater_on_outside_tolerance(self): """Test if temperature change turn heater on outside cold tolerance.""" self._setup_switch(False) - climate.set_temperature(self.hass, 30) + common.set_temperature(self.hass, 30) self.hass.block_till_done() self._setup_sensor(27) self.hass.block_till_done() @@ -282,7 +283,7 @@ class TestClimateGenericThermostat(unittest.TestCase): def test_temp_change_heater_off_within_tolerance(self): """Test if temperature change doesn't turn off within tolerance.""" self._setup_switch(True) - climate.set_temperature(self.hass, 30) + common.set_temperature(self.hass, 30) self.hass.block_till_done() self._setup_sensor(33) self.hass.block_till_done() @@ -291,7 +292,7 @@ class TestClimateGenericThermostat(unittest.TestCase): def test_temp_change_heater_off_outside_tolerance(self): """Test if temperature change turn heater off outside hot tolerance.""" self._setup_switch(True) - climate.set_temperature(self.hass, 30) + common.set_temperature(self.hass, 30) self.hass.block_till_done() self._setup_sensor(35) self.hass.block_till_done() @@ -304,9 +305,9 @@ class TestClimateGenericThermostat(unittest.TestCase): def test_running_when_operating_mode_is_off(self): """Test that the switch turns off when enabled is set False.""" self._setup_switch(True) - climate.set_temperature(self.hass, 30) + common.set_temperature(self.hass, 30) self.hass.block_till_done() - climate.set_operation_mode(self.hass, STATE_OFF) + common.set_operation_mode(self.hass, STATE_OFF) self.hass.block_till_done() self.assertEqual(1, len(self.calls)) call = self.calls[0] @@ -317,9 +318,9 @@ class TestClimateGenericThermostat(unittest.TestCase): def test_no_state_change_when_operation_mode_off(self): """Test that the switch doesn't turn on when enabled is False.""" self._setup_switch(False) - climate.set_temperature(self.hass, 30) + common.set_temperature(self.hass, 30) self.hass.block_till_done() - climate.set_operation_mode(self.hass, STATE_OFF) + common.set_operation_mode(self.hass, STATE_OFF) self.hass.block_till_done() self._setup_sensor(25) self.hass.block_till_done() @@ -328,7 +329,7 @@ class TestClimateGenericThermostat(unittest.TestCase): @mock.patch('logging.Logger.error') def test_invalid_operating_mode(self, log_mock): """Test error handling for invalid operation mode.""" - climate.set_operation_mode(self.hass, 'invalid mode') + common.set_operation_mode(self.hass, 'invalid mode') self.hass.block_till_done() self.assertEqual(log_mock.call_count, 1) @@ -337,12 +338,12 @@ class TestClimateGenericThermostat(unittest.TestCase): Switch turns on when temp below setpoint and mode changes. """ - climate.set_operation_mode(self.hass, STATE_OFF) - climate.set_temperature(self.hass, 30) + common.set_operation_mode(self.hass, STATE_OFF) + common.set_temperature(self.hass, 30) self._setup_sensor(25) self.hass.block_till_done() self._setup_switch(False) - climate.set_operation_mode(self.hass, climate.STATE_HEAT) + common.set_operation_mode(self.hass, climate.STATE_HEAT) self.hass.block_till_done() self.assertEqual(1, len(self.calls)) call = self.calls[0] @@ -395,7 +396,7 @@ class TestClimateGenericThermostatACMode(unittest.TestCase): self._setup_switch(True) self._setup_sensor(25) self.hass.block_till_done() - climate.set_temperature(self.hass, 30) + common.set_temperature(self.hass, 30) self.hass.block_till_done() self.assertEqual(2, len(self.calls)) call = self.calls[0] @@ -407,9 +408,9 @@ class TestClimateGenericThermostatACMode(unittest.TestCase): """Test the setting away mode when cooling.""" self._setup_sensor(25) self.hass.block_till_done() - climate.set_temperature(self.hass, 19) + common.set_temperature(self.hass, 19) self.hass.block_till_done() - climate.set_away_mode(self.hass, True) + common.set_away_mode(self.hass, True) self.hass.block_till_done() state = self.hass.states.get(ENTITY) self.assertEqual(30, state.attributes.get('temperature')) @@ -419,12 +420,12 @@ class TestClimateGenericThermostatACMode(unittest.TestCase): Switch turns on when temp below setpoint and mode changes. """ - climate.set_operation_mode(self.hass, STATE_OFF) - climate.set_temperature(self.hass, 25) + common.set_operation_mode(self.hass, STATE_OFF) + common.set_temperature(self.hass, 25) self._setup_sensor(30) self.hass.block_till_done() self._setup_switch(False) - climate.set_operation_mode(self.hass, climate.STATE_COOL) + common.set_operation_mode(self.hass, climate.STATE_COOL) self.hass.block_till_done() self.assertEqual(1, len(self.calls)) call = self.calls[0] @@ -437,7 +438,7 @@ class TestClimateGenericThermostatACMode(unittest.TestCase): self._setup_switch(False) self._setup_sensor(30) self.hass.block_till_done() - climate.set_temperature(self.hass, 25) + common.set_temperature(self.hass, 25) self.hass.block_till_done() self.assertEqual(1, len(self.calls)) call = self.calls[0] @@ -448,7 +449,7 @@ class TestClimateGenericThermostatACMode(unittest.TestCase): def test_temp_change_ac_off_within_tolerance(self): """Test if temperature change doesn't turn ac off within tolerance.""" self._setup_switch(True) - climate.set_temperature(self.hass, 30) + common.set_temperature(self.hass, 30) self.hass.block_till_done() self._setup_sensor(29.8) self.hass.block_till_done() @@ -457,7 +458,7 @@ class TestClimateGenericThermostatACMode(unittest.TestCase): def test_set_temp_change_ac_off_outside_tolerance(self): """Test if temperature change turn ac off.""" self._setup_switch(True) - climate.set_temperature(self.hass, 30) + common.set_temperature(self.hass, 30) self.hass.block_till_done() self._setup_sensor(27) self.hass.block_till_done() @@ -470,7 +471,7 @@ class TestClimateGenericThermostatACMode(unittest.TestCase): def test_temp_change_ac_on_within_tolerance(self): """Test if temperature change doesn't turn ac on within tolerance.""" self._setup_switch(False) - climate.set_temperature(self.hass, 25) + common.set_temperature(self.hass, 25) self.hass.block_till_done() self._setup_sensor(25.2) self.hass.block_till_done() @@ -479,7 +480,7 @@ class TestClimateGenericThermostatACMode(unittest.TestCase): def test_temp_change_ac_on_outside_tolerance(self): """Test if temperature change turn ac on.""" self._setup_switch(False) - climate.set_temperature(self.hass, 25) + common.set_temperature(self.hass, 25) self.hass.block_till_done() self._setup_sensor(30) self.hass.block_till_done() @@ -492,9 +493,9 @@ class TestClimateGenericThermostatACMode(unittest.TestCase): def test_running_when_operating_mode_is_off(self): """Test that the switch turns off when enabled is set False.""" self._setup_switch(True) - climate.set_temperature(self.hass, 30) + common.set_temperature(self.hass, 30) self.hass.block_till_done() - climate.set_operation_mode(self.hass, STATE_OFF) + common.set_operation_mode(self.hass, STATE_OFF) self.hass.block_till_done() self.assertEqual(1, len(self.calls)) call = self.calls[0] @@ -505,9 +506,9 @@ class TestClimateGenericThermostatACMode(unittest.TestCase): def test_no_state_change_when_operation_mode_off(self): """Test that the switch doesn't turn on when enabled is False.""" self._setup_switch(False) - climate.set_temperature(self.hass, 30) + common.set_temperature(self.hass, 30) self.hass.block_till_done() - climate.set_operation_mode(self.hass, STATE_OFF) + common.set_operation_mode(self.hass, STATE_OFF) self.hass.block_till_done() self._setup_sensor(35) self.hass.block_till_done() @@ -556,7 +557,7 @@ class TestClimateGenericThermostatACModeMinCycle(unittest.TestCase): def test_temp_change_ac_trigger_on_not_long_enough(self): """Test if temperature change turn ac on.""" self._setup_switch(False) - climate.set_temperature(self.hass, 25) + common.set_temperature(self.hass, 25) self.hass.block_till_done() self._setup_sensor(30) self.hass.block_till_done() @@ -569,7 +570,7 @@ class TestClimateGenericThermostatACModeMinCycle(unittest.TestCase): with mock.patch('homeassistant.helpers.condition.dt_util.utcnow', return_value=fake_changed): self._setup_switch(False) - climate.set_temperature(self.hass, 25) + common.set_temperature(self.hass, 25) self.hass.block_till_done() self._setup_sensor(30) self.hass.block_till_done() @@ -582,7 +583,7 @@ class TestClimateGenericThermostatACModeMinCycle(unittest.TestCase): def test_temp_change_ac_trigger_off_not_long_enough(self): """Test if temperature change turn ac on.""" self._setup_switch(True) - climate.set_temperature(self.hass, 30) + common.set_temperature(self.hass, 30) self.hass.block_till_done() self._setup_sensor(25) self.hass.block_till_done() @@ -595,7 +596,7 @@ class TestClimateGenericThermostatACModeMinCycle(unittest.TestCase): with mock.patch('homeassistant.helpers.condition.dt_util.utcnow', return_value=fake_changed): self._setup_switch(True) - climate.set_temperature(self.hass, 30) + common.set_temperature(self.hass, 30) self.hass.block_till_done() self._setup_sensor(25) self.hass.block_till_done() @@ -647,7 +648,7 @@ class TestClimateGenericThermostatMinCycle(unittest.TestCase): def test_temp_change_heater_trigger_off_not_long_enough(self): """Test if temp change doesn't turn heater off because of time.""" self._setup_switch(True) - climate.set_temperature(self.hass, 25) + common.set_temperature(self.hass, 25) self.hass.block_till_done() self._setup_sensor(30) self.hass.block_till_done() @@ -656,7 +657,7 @@ class TestClimateGenericThermostatMinCycle(unittest.TestCase): def test_temp_change_heater_trigger_on_not_long_enough(self): """Test if temp change doesn't turn heater on because of time.""" self._setup_switch(False) - climate.set_temperature(self.hass, 30) + common.set_temperature(self.hass, 30) self.hass.block_till_done() self._setup_sensor(25) self.hass.block_till_done() @@ -669,7 +670,7 @@ class TestClimateGenericThermostatMinCycle(unittest.TestCase): with mock.patch('homeassistant.helpers.condition.dt_util.utcnow', return_value=fake_changed): self._setup_switch(False) - climate.set_temperature(self.hass, 30) + common.set_temperature(self.hass, 30) self.hass.block_till_done() self._setup_sensor(25) self.hass.block_till_done() @@ -686,7 +687,7 @@ class TestClimateGenericThermostatMinCycle(unittest.TestCase): with mock.patch('homeassistant.helpers.condition.dt_util.utcnow', return_value=fake_changed): self._setup_switch(True) - climate.set_temperature(self.hass, 25) + common.set_temperature(self.hass, 25) self.hass.block_till_done() self._setup_sensor(30) self.hass.block_till_done() @@ -743,7 +744,7 @@ class TestClimateGenericThermostatACKeepAlive(unittest.TestCase): self.hass.block_till_done() self._setup_sensor(30) self.hass.block_till_done() - climate.set_temperature(self.hass, 25) + common.set_temperature(self.hass, 25) self.hass.block_till_done() test_time = datetime.datetime.now(pytz.UTC) self._send_time_changed(test_time) @@ -766,7 +767,7 @@ class TestClimateGenericThermostatACKeepAlive(unittest.TestCase): self.hass.block_till_done() self._setup_sensor(20) self.hass.block_till_done() - climate.set_temperature(self.hass, 25) + common.set_temperature(self.hass, 25) self.hass.block_till_done() test_time = datetime.datetime.now(pytz.UTC) self._send_time_changed(test_time) @@ -833,7 +834,7 @@ class TestClimateGenericThermostatKeepAlive(unittest.TestCase): self.hass.block_till_done() self._setup_sensor(20) self.hass.block_till_done() - climate.set_temperature(self.hass, 25) + common.set_temperature(self.hass, 25) self.hass.block_till_done() test_time = datetime.datetime.now(pytz.UTC) self._send_time_changed(test_time) @@ -856,7 +857,7 @@ class TestClimateGenericThermostatKeepAlive(unittest.TestCase): self.hass.block_till_done() self._setup_sensor(30) self.hass.block_till_done() - climate.set_temperature(self.hass, 25) + common.set_temperature(self.hass, 25) self.hass.block_till_done() test_time = datetime.datetime.now(pytz.UTC) self._send_time_changed(test_time) @@ -926,7 +927,7 @@ class TestClimateGenericThermostatTurnOnOff(unittest.TestCase): def test_turn_on_when_off(self): """Test if climate.turn_on turns on a turned off device.""" - climate.set_operation_mode(self.hass, STATE_OFF) + common.set_operation_mode(self.hass, STATE_OFF) self.hass.block_till_done() self.hass.services.call('climate', SERVICE_TURN_ON) self.hass.block_till_done() @@ -939,8 +940,8 @@ class TestClimateGenericThermostatTurnOnOff(unittest.TestCase): def test_turn_on_when_on(self): """Test if climate.turn_on does nothing to a turned on device.""" - climate.set_operation_mode(self.hass, STATE_HEAT, self.HEAT_ENTITY) - climate.set_operation_mode(self.hass, STATE_COOL, self.COOL_ENTITY) + common.set_operation_mode(self.hass, STATE_HEAT, self.HEAT_ENTITY) + common.set_operation_mode(self.hass, STATE_COOL, self.COOL_ENTITY) self.hass.block_till_done() self.hass.services.call('climate', SERVICE_TURN_ON) self.hass.block_till_done() @@ -953,8 +954,8 @@ class TestClimateGenericThermostatTurnOnOff(unittest.TestCase): def test_turn_off_when_on(self): """Test if climate.turn_off turns off a turned on device.""" - climate.set_operation_mode(self.hass, STATE_HEAT, self.HEAT_ENTITY) - climate.set_operation_mode(self.hass, STATE_COOL, self.COOL_ENTITY) + common.set_operation_mode(self.hass, STATE_HEAT, self.HEAT_ENTITY) + common.set_operation_mode(self.hass, STATE_COOL, self.COOL_ENTITY) self.hass.block_till_done() self.hass.services.call('climate', SERVICE_TURN_OFF) self.hass.block_till_done() @@ -967,7 +968,7 @@ class TestClimateGenericThermostatTurnOnOff(unittest.TestCase): def test_turn_off_when_off(self): """Test if climate.turn_off does nothing to a turned off device.""" - climate.set_operation_mode(self.hass, STATE_OFF) + common.set_operation_mode(self.hass, STATE_OFF) self.hass.block_till_done() self.hass.services.call('climate', SERVICE_TURN_OFF) self.hass.block_till_done() diff --git a/tests/components/climate/test_mqtt.py b/tests/components/climate/test_mqtt.py index 6c43c4d9dbe..1fdf72e34ba 100644 --- a/tests/components/climate/test_mqtt.py +++ b/tests/components/climate/test_mqtt.py @@ -16,6 +16,7 @@ from homeassistant.components.mqtt.discovery import async_start from tests.common import (get_test_home_assistant, mock_mqtt_component, async_fire_mqtt_message, fire_mqtt_message, mock_component) +from tests.components.climate import common ENTITY_CLIMATE = 'climate.test' @@ -90,7 +91,7 @@ class TestMQTTClimate(unittest.TestCase): state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual("off", state.attributes.get('operation_mode')) self.assertEqual("off", state.state) - climate.set_operation_mode(self.hass, None, ENTITY_CLIMATE) + common.set_operation_mode(self.hass, None, ENTITY_CLIMATE) self.hass.block_till_done() state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual("off", state.attributes.get('operation_mode')) @@ -103,7 +104,7 @@ class TestMQTTClimate(unittest.TestCase): state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual("off", state.attributes.get('operation_mode')) self.assertEqual("off", state.state) - climate.set_operation_mode(self.hass, "cool", ENTITY_CLIMATE) + common.set_operation_mode(self.hass, "cool", ENTITY_CLIMATE) self.hass.block_till_done() state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual("cool", state.attributes.get('operation_mode')) @@ -121,7 +122,7 @@ class TestMQTTClimate(unittest.TestCase): self.assertEqual("off", state.attributes.get('operation_mode')) self.assertEqual("off", state.state) - climate.set_operation_mode(self.hass, "cool", ENTITY_CLIMATE) + common.set_operation_mode(self.hass, "cool", ENTITY_CLIMATE) self.hass.block_till_done() state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual("off", state.attributes.get('operation_mode')) @@ -148,7 +149,7 @@ class TestMQTTClimate(unittest.TestCase): state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual("off", state.attributes.get('operation_mode')) self.assertEqual("off", state.state) - climate.set_operation_mode(self.hass, "on", ENTITY_CLIMATE) + common.set_operation_mode(self.hass, "on", ENTITY_CLIMATE) self.hass.block_till_done() state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual("on", state.attributes.get('operation_mode')) @@ -159,7 +160,7 @@ class TestMQTTClimate(unittest.TestCase): ]) self.mock_publish.async_publish.reset_mock() - climate.set_operation_mode(self.hass, "off", ENTITY_CLIMATE) + common.set_operation_mode(self.hass, "off", ENTITY_CLIMATE) self.hass.block_till_done() state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual("off", state.attributes.get('operation_mode')) @@ -176,7 +177,7 @@ class TestMQTTClimate(unittest.TestCase): state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual("low", state.attributes.get('fan_mode')) - climate.set_fan_mode(self.hass, None, ENTITY_CLIMATE) + common.set_fan_mode(self.hass, None, ENTITY_CLIMATE) self.hass.block_till_done() state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual("low", state.attributes.get('fan_mode')) @@ -190,7 +191,7 @@ class TestMQTTClimate(unittest.TestCase): state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual("low", state.attributes.get('fan_mode')) - climate.set_fan_mode(self.hass, 'high', ENTITY_CLIMATE) + common.set_fan_mode(self.hass, 'high', ENTITY_CLIMATE) self.hass.block_till_done() state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual("low", state.attributes.get('fan_mode')) @@ -211,7 +212,7 @@ class TestMQTTClimate(unittest.TestCase): state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual("low", state.attributes.get('fan_mode')) - climate.set_fan_mode(self.hass, 'high', ENTITY_CLIMATE) + common.set_fan_mode(self.hass, 'high', ENTITY_CLIMATE) self.hass.block_till_done() self.mock_publish.async_publish.assert_called_once_with( 'fan-mode-topic', 'high', 0, False) @@ -224,7 +225,7 @@ class TestMQTTClimate(unittest.TestCase): state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual("off", state.attributes.get('swing_mode')) - climate.set_swing_mode(self.hass, None, ENTITY_CLIMATE) + common.set_swing_mode(self.hass, None, ENTITY_CLIMATE) self.hass.block_till_done() state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual("off", state.attributes.get('swing_mode')) @@ -238,7 +239,7 @@ class TestMQTTClimate(unittest.TestCase): state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual("off", state.attributes.get('swing_mode')) - climate.set_swing_mode(self.hass, 'on', ENTITY_CLIMATE) + common.set_swing_mode(self.hass, 'on', ENTITY_CLIMATE) self.hass.block_till_done() state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual("off", state.attributes.get('swing_mode')) @@ -259,7 +260,7 @@ class TestMQTTClimate(unittest.TestCase): state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual("off", state.attributes.get('swing_mode')) - climate.set_swing_mode(self.hass, 'on', ENTITY_CLIMATE) + common.set_swing_mode(self.hass, 'on', ENTITY_CLIMATE) self.hass.block_till_done() self.mock_publish.async_publish.assert_called_once_with( 'swing-mode-topic', 'on', 0, False) @@ -272,15 +273,15 @@ class TestMQTTClimate(unittest.TestCase): state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual(21, state.attributes.get('temperature')) - climate.set_operation_mode(self.hass, 'heat', ENTITY_CLIMATE) + common.set_operation_mode(self.hass, 'heat', ENTITY_CLIMATE) self.hass.block_till_done() state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual('heat', state.attributes.get('operation_mode')) self.mock_publish.async_publish.assert_called_once_with( 'mode-topic', 'heat', 0, False) self.mock_publish.async_publish.reset_mock() - climate.set_temperature(self.hass, temperature=47, - entity_id=ENTITY_CLIMATE) + common.set_temperature(self.hass, temperature=47, + entity_id=ENTITY_CLIMATE) self.hass.block_till_done() state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual(47, state.attributes.get('temperature')) @@ -289,9 +290,9 @@ class TestMQTTClimate(unittest.TestCase): # also test directly supplying the operation mode to set_temperature self.mock_publish.async_publish.reset_mock() - climate.set_temperature(self.hass, temperature=21, - operation_mode="cool", - entity_id=ENTITY_CLIMATE) + common.set_temperature(self.hass, temperature=21, + operation_mode="cool", + entity_id=ENTITY_CLIMATE) self.hass.block_till_done() state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual('cool', state.attributes.get('operation_mode')) @@ -310,10 +311,10 @@ class TestMQTTClimate(unittest.TestCase): state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual(21, state.attributes.get('temperature')) - climate.set_operation_mode(self.hass, 'heat', ENTITY_CLIMATE) + common.set_operation_mode(self.hass, 'heat', ENTITY_CLIMATE) self.hass.block_till_done() - climate.set_temperature(self.hass, temperature=47, - entity_id=ENTITY_CLIMATE) + common.set_temperature(self.hass, temperature=47, + entity_id=ENTITY_CLIMATE) self.hass.block_till_done() state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual(21, state.attributes.get('temperature')) @@ -349,7 +350,7 @@ class TestMQTTClimate(unittest.TestCase): state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual('off', state.attributes.get('away_mode')) - climate.set_away_mode(self.hass, True, ENTITY_CLIMATE) + common.set_away_mode(self.hass, True, ENTITY_CLIMATE) self.hass.block_till_done() state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual('off', state.attributes.get('away_mode')) @@ -379,7 +380,7 @@ class TestMQTTClimate(unittest.TestCase): state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual('off', state.attributes.get('away_mode')) - climate.set_away_mode(self.hass, True, ENTITY_CLIMATE) + common.set_away_mode(self.hass, True, ENTITY_CLIMATE) self.hass.block_till_done() self.mock_publish.async_publish.assert_called_once_with( 'away-mode-topic', 'AN', 0, False) @@ -387,7 +388,7 @@ class TestMQTTClimate(unittest.TestCase): state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual('on', state.attributes.get('away_mode')) - climate.set_away_mode(self.hass, False, ENTITY_CLIMATE) + common.set_away_mode(self.hass, False, ENTITY_CLIMATE) self.hass.block_till_done() self.mock_publish.async_publish.assert_called_once_with( 'away-mode-topic', 'AUS', 0, False) @@ -403,7 +404,7 @@ class TestMQTTClimate(unittest.TestCase): state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual(None, state.attributes.get('hold_mode')) - climate.set_hold_mode(self.hass, 'on', ENTITY_CLIMATE) + common.set_hold_mode(self.hass, 'on', ENTITY_CLIMATE) self.hass.block_till_done() state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual(None, state.attributes.get('hold_mode')) @@ -424,7 +425,7 @@ class TestMQTTClimate(unittest.TestCase): state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual(None, state.attributes.get('hold_mode')) - climate.set_hold_mode(self.hass, 'on', ENTITY_CLIMATE) + common.set_hold_mode(self.hass, 'on', ENTITY_CLIMATE) self.hass.block_till_done() self.mock_publish.async_publish.assert_called_once_with( 'hold-topic', 'on', 0, False) @@ -432,7 +433,7 @@ class TestMQTTClimate(unittest.TestCase): state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual('on', state.attributes.get('hold_mode')) - climate.set_hold_mode(self.hass, 'off', ENTITY_CLIMATE) + common.set_hold_mode(self.hass, 'off', ENTITY_CLIMATE) self.hass.block_till_done() self.mock_publish.async_publish.assert_called_once_with( 'hold-topic', 'off', 0, False) @@ -448,7 +449,7 @@ class TestMQTTClimate(unittest.TestCase): state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual('off', state.attributes.get('aux_heat')) - climate.set_aux_heat(self.hass, True, ENTITY_CLIMATE) + common.set_aux_heat(self.hass, True, ENTITY_CLIMATE) self.hass.block_till_done() state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual('off', state.attributes.get('aux_heat')) @@ -474,7 +475,7 @@ class TestMQTTClimate(unittest.TestCase): state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual('off', state.attributes.get('aux_heat')) - climate.set_aux_heat(self.hass, True, ENTITY_CLIMATE) + common.set_aux_heat(self.hass, True, ENTITY_CLIMATE) self.hass.block_till_done() self.mock_publish.async_publish.assert_called_once_with( 'aux-topic', 'ON', 0, False) @@ -482,7 +483,7 @@ class TestMQTTClimate(unittest.TestCase): state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual('on', state.attributes.get('aux_heat')) - climate.set_aux_heat(self.hass, False, ENTITY_CLIMATE) + common.set_aux_heat(self.hass, False, ENTITY_CLIMATE) self.hass.block_till_done() self.mock_publish.async_publish.assert_called_once_with( 'aux-topic', 'OFF', 0, False) diff --git a/tests/components/counter/common.py b/tests/components/counter/common.py new file mode 100644 index 00000000000..36d09979d0d --- /dev/null +++ b/tests/components/counter/common.py @@ -0,0 +1,52 @@ +"""Collection of helper methods. + +All containing methods are legacy helpers that should not be used by new +components. Instead call the service directly. +""" +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.components.counter import ( + DOMAIN, SERVICE_DECREMENT, SERVICE_INCREMENT, SERVICE_RESET) +from homeassistant.core import callback +from homeassistant.loader import bind_hass + + +@bind_hass +def increment(hass, entity_id): + """Increment a counter.""" + hass.add_job(async_increment, hass, entity_id) + + +@callback +@bind_hass +def async_increment(hass, entity_id): + """Increment a counter.""" + hass.async_add_job(hass.services.async_call( + DOMAIN, SERVICE_INCREMENT, {ATTR_ENTITY_ID: entity_id})) + + +@bind_hass +def decrement(hass, entity_id): + """Decrement a counter.""" + hass.add_job(async_decrement, hass, entity_id) + + +@callback +@bind_hass +def async_decrement(hass, entity_id): + """Decrement a counter.""" + hass.async_add_job(hass.services.async_call( + DOMAIN, SERVICE_DECREMENT, {ATTR_ENTITY_ID: entity_id})) + + +@bind_hass +def reset(hass, entity_id): + """Reset a counter.""" + hass.add_job(async_reset, hass, entity_id) + + +@callback +@bind_hass +def async_reset(hass, entity_id): + """Reset a counter.""" + hass.async_add_job(hass.services.async_call( + DOMAIN, SERVICE_RESET, {ATTR_ENTITY_ID: entity_id})) diff --git a/tests/components/counter/test_init.py b/tests/components/counter/test_init.py index af36c1c8f95..e5e0ee594ac 100644 --- a/tests/components/counter/test_init.py +++ b/tests/components/counter/test_init.py @@ -7,11 +7,11 @@ import logging from homeassistant.core import CoreState, State, Context from homeassistant.setup import setup_component, async_setup_component from homeassistant.components.counter import ( - DOMAIN, decrement, increment, reset, CONF_INITIAL, CONF_STEP, CONF_NAME, - CONF_ICON) + DOMAIN, CONF_INITIAL, CONF_STEP, CONF_NAME, CONF_ICON) from homeassistant.const import (ATTR_ICON, ATTR_FRIENDLY_NAME) from tests.common import (get_test_home_assistant, mock_restore_cache) +from tests.components.counter.common import decrement, increment, reset _LOGGER = logging.getLogger(__name__) diff --git a/tests/components/fan/common.py b/tests/components/fan/common.py new file mode 100644 index 00000000000..d4caf28be5b --- /dev/null +++ b/tests/components/fan/common.py @@ -0,0 +1,82 @@ +"""Collection of helper methods. + +All containing methods are legacy helpers that should not be used by new +components. Instead call the service directly. +""" +from homeassistant.components.fan import ( + ATTR_DIRECTION, ATTR_SPEED, ATTR_OSCILLATING, DOMAIN, + SERVICE_OSCILLATE, SERVICE_SET_DIRECTION, SERVICE_SET_SPEED) +from homeassistant.const import ( + ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TOGGLE, SERVICE_TURN_OFF) +from homeassistant.loader import bind_hass + + +@bind_hass +def turn_on(hass, entity_id: str = None, speed: str = None) -> None: + """Turn all or specified fan on.""" + data = { + key: value for key, value in [ + (ATTR_ENTITY_ID, entity_id), + (ATTR_SPEED, speed), + ] if value is not None + } + + hass.services.call(DOMAIN, SERVICE_TURN_ON, data) + + +@bind_hass +def turn_off(hass, entity_id: str = None) -> None: + """Turn all or specified fan off.""" + data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} + + hass.services.call(DOMAIN, SERVICE_TURN_OFF, data) + + +@bind_hass +def toggle(hass, entity_id: str = None) -> None: + """Toggle all or specified fans.""" + data = { + ATTR_ENTITY_ID: entity_id + } + + hass.services.call(DOMAIN, SERVICE_TOGGLE, data) + + +@bind_hass +def oscillate(hass, entity_id: str = None, + should_oscillate: bool = True) -> None: + """Set oscillation on all or specified fan.""" + data = { + key: value for key, value in [ + (ATTR_ENTITY_ID, entity_id), + (ATTR_OSCILLATING, should_oscillate), + ] if value is not None + } + + hass.services.call(DOMAIN, SERVICE_OSCILLATE, data) + + +@bind_hass +def set_speed(hass, entity_id: str = None, speed: str = None) -> None: + """Set speed for all or specified fan.""" + data = { + key: value for key, value in [ + (ATTR_ENTITY_ID, entity_id), + (ATTR_SPEED, speed), + ] if value is not None + } + + hass.services.call(DOMAIN, SERVICE_SET_SPEED, data) + + +@bind_hass +def set_direction(hass, entity_id: str = None, direction: str = None) -> None: + """Set direction for all or specified fan.""" + data = { + key: value for key, value in [ + (ATTR_ENTITY_ID, entity_id), + (ATTR_DIRECTION, direction), + ] if value is not None + } + + hass.services.call(DOMAIN, SERVICE_SET_DIRECTION, data) diff --git a/tests/components/fan/test_demo.py b/tests/components/fan/test_demo.py index 69680fb1cfd..48704ca4464 100644 --- a/tests/components/fan/test_demo.py +++ b/tests/components/fan/test_demo.py @@ -7,6 +7,7 @@ from homeassistant.components import fan from homeassistant.const import STATE_OFF, STATE_ON from tests.common import get_test_home_assistant +from tests.components.fan import common FAN_ENTITY_ID = 'fan.living_room_fan' @@ -34,11 +35,11 @@ class TestDemoFan(unittest.TestCase): """Test turning on the device.""" self.assertEqual(STATE_OFF, self.get_entity().state) - fan.turn_on(self.hass, FAN_ENTITY_ID) + common.turn_on(self.hass, FAN_ENTITY_ID) self.hass.block_till_done() self.assertNotEqual(STATE_OFF, self.get_entity().state) - fan.turn_on(self.hass, FAN_ENTITY_ID, fan.SPEED_HIGH) + common.turn_on(self.hass, FAN_ENTITY_ID, fan.SPEED_HIGH) self.hass.block_till_done() self.assertEqual(STATE_ON, self.get_entity().state) self.assertEqual(fan.SPEED_HIGH, @@ -48,11 +49,11 @@ class TestDemoFan(unittest.TestCase): """Test turning off the device.""" self.assertEqual(STATE_OFF, self.get_entity().state) - fan.turn_on(self.hass, FAN_ENTITY_ID) + common.turn_on(self.hass, FAN_ENTITY_ID) self.hass.block_till_done() self.assertNotEqual(STATE_OFF, self.get_entity().state) - fan.turn_off(self.hass, FAN_ENTITY_ID) + common.turn_off(self.hass, FAN_ENTITY_ID) self.hass.block_till_done() self.assertEqual(STATE_OFF, self.get_entity().state) @@ -60,11 +61,11 @@ class TestDemoFan(unittest.TestCase): """Test turning off all fans.""" self.assertEqual(STATE_OFF, self.get_entity().state) - fan.turn_on(self.hass, FAN_ENTITY_ID) + common.turn_on(self.hass, FAN_ENTITY_ID) self.hass.block_till_done() self.assertNotEqual(STATE_OFF, self.get_entity().state) - fan.turn_off(self.hass) + common.turn_off(self.hass) self.hass.block_till_done() self.assertEqual(STATE_OFF, self.get_entity().state) @@ -72,7 +73,7 @@ class TestDemoFan(unittest.TestCase): """Test setting the direction of the device.""" self.assertEqual(STATE_OFF, self.get_entity().state) - fan.set_direction(self.hass, FAN_ENTITY_ID, fan.DIRECTION_REVERSE) + common.set_direction(self.hass, FAN_ENTITY_ID, fan.DIRECTION_REVERSE) self.hass.block_till_done() self.assertEqual(fan.DIRECTION_REVERSE, self.get_entity().attributes.get('direction')) @@ -81,7 +82,7 @@ class TestDemoFan(unittest.TestCase): """Test setting the speed of the device.""" self.assertEqual(STATE_OFF, self.get_entity().state) - fan.set_speed(self.hass, FAN_ENTITY_ID, fan.SPEED_LOW) + common.set_speed(self.hass, FAN_ENTITY_ID, fan.SPEED_LOW) self.hass.block_till_done() self.assertEqual(fan.SPEED_LOW, self.get_entity().attributes.get('speed')) @@ -90,11 +91,11 @@ class TestDemoFan(unittest.TestCase): """Test oscillating the fan.""" self.assertFalse(self.get_entity().attributes.get('oscillating')) - fan.oscillate(self.hass, FAN_ENTITY_ID, True) + common.oscillate(self.hass, FAN_ENTITY_ID, True) self.hass.block_till_done() self.assertTrue(self.get_entity().attributes.get('oscillating')) - fan.oscillate(self.hass, FAN_ENTITY_ID, False) + common.oscillate(self.hass, FAN_ENTITY_ID, False) self.hass.block_till_done() self.assertFalse(self.get_entity().attributes.get('oscillating')) @@ -102,6 +103,6 @@ class TestDemoFan(unittest.TestCase): """Test is on service call.""" self.assertFalse(fan.is_on(self.hass, FAN_ENTITY_ID)) - fan.turn_on(self.hass, FAN_ENTITY_ID) + common.turn_on(self.hass, FAN_ENTITY_ID) self.hass.block_till_done() self.assertTrue(fan.is_on(self.hass, FAN_ENTITY_ID)) diff --git a/tests/components/fan/test_template.py b/tests/components/fan/test_template.py index e229083069d..09d3603e004 100644 --- a/tests/components/fan/test_template.py +++ b/tests/components/fan/test_template.py @@ -3,7 +3,6 @@ import logging from homeassistant.core import callback from homeassistant import setup -import homeassistant.components as components from homeassistant.const import STATE_ON, STATE_OFF from homeassistant.components.fan import ( ATTR_SPEED, ATTR_OSCILLATING, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH, @@ -11,6 +10,8 @@ from homeassistant.components.fan import ( from tests.common import ( get_test_home_assistant, assert_setup_component) +from tests.components.fan import common + _LOGGER = logging.getLogger(__name__) @@ -288,7 +289,7 @@ class TestTemplateFan: self._register_components() # Turn on fan - components.fan.turn_on(self.hass, _TEST_FAN) + common.turn_on(self.hass, _TEST_FAN) self.hass.block_till_done() # verify @@ -296,7 +297,7 @@ class TestTemplateFan: self._verify(STATE_ON, None, None, None) # Turn off fan - components.fan.turn_off(self.hass, _TEST_FAN) + common.turn_off(self.hass, _TEST_FAN) self.hass.block_till_done() # verify @@ -308,7 +309,7 @@ class TestTemplateFan: self._register_components() # Turn on fan with high speed - components.fan.turn_on(self.hass, _TEST_FAN, SPEED_HIGH) + common.turn_on(self.hass, _TEST_FAN, SPEED_HIGH) self.hass.block_till_done() # verify @@ -321,11 +322,11 @@ class TestTemplateFan: self._register_components() # Turn on fan - components.fan.turn_on(self.hass, _TEST_FAN) + common.turn_on(self.hass, _TEST_FAN) self.hass.block_till_done() # Set fan's speed to high - components.fan.set_speed(self.hass, _TEST_FAN, SPEED_HIGH) + common.set_speed(self.hass, _TEST_FAN, SPEED_HIGH) self.hass.block_till_done() # verify @@ -333,7 +334,7 @@ class TestTemplateFan: self._verify(STATE_ON, SPEED_HIGH, None, None) # Set fan's speed to medium - components.fan.set_speed(self.hass, _TEST_FAN, SPEED_MEDIUM) + common.set_speed(self.hass, _TEST_FAN, SPEED_MEDIUM) self.hass.block_till_done() # verify @@ -345,11 +346,11 @@ class TestTemplateFan: self._register_components() # Turn on fan - components.fan.turn_on(self.hass, _TEST_FAN) + common.turn_on(self.hass, _TEST_FAN) self.hass.block_till_done() # Set fan's speed to 'invalid' - components.fan.set_speed(self.hass, _TEST_FAN, 'invalid') + common.set_speed(self.hass, _TEST_FAN, 'invalid') self.hass.block_till_done() # verify speed is unchanged @@ -361,11 +362,11 @@ class TestTemplateFan: self._register_components() # Turn on fan - components.fan.turn_on(self.hass, _TEST_FAN) + common.turn_on(self.hass, _TEST_FAN) self.hass.block_till_done() # Set fan's speed to high - components.fan.set_speed(self.hass, _TEST_FAN, SPEED_HIGH) + common.set_speed(self.hass, _TEST_FAN, SPEED_HIGH) self.hass.block_till_done() # verify @@ -373,7 +374,7 @@ class TestTemplateFan: self._verify(STATE_ON, SPEED_HIGH, None, None) # Set fan's speed to 'invalid' - components.fan.set_speed(self.hass, _TEST_FAN, 'invalid') + common.set_speed(self.hass, _TEST_FAN, 'invalid') self.hass.block_till_done() # verify speed is unchanged @@ -385,11 +386,11 @@ class TestTemplateFan: self._register_components(['1', '2', '3']) # Turn on fan - components.fan.turn_on(self.hass, _TEST_FAN) + common.turn_on(self.hass, _TEST_FAN) self.hass.block_till_done() # Set fan's speed to '1' - components.fan.set_speed(self.hass, _TEST_FAN, '1') + common.set_speed(self.hass, _TEST_FAN, '1') self.hass.block_till_done() # verify @@ -397,7 +398,7 @@ class TestTemplateFan: self._verify(STATE_ON, '1', None, None) # Set fan's speed to 'medium' which is invalid - components.fan.set_speed(self.hass, _TEST_FAN, SPEED_MEDIUM) + common.set_speed(self.hass, _TEST_FAN, SPEED_MEDIUM) self.hass.block_till_done() # verify that speed is unchanged @@ -409,11 +410,11 @@ class TestTemplateFan: self._register_components() # Turn on fan - components.fan.turn_on(self.hass, _TEST_FAN) + common.turn_on(self.hass, _TEST_FAN) self.hass.block_till_done() # Set fan's osc to True - components.fan.oscillate(self.hass, _TEST_FAN, True) + common.oscillate(self.hass, _TEST_FAN, True) self.hass.block_till_done() # verify @@ -421,7 +422,7 @@ class TestTemplateFan: self._verify(STATE_ON, None, True, None) # Set fan's osc to False - components.fan.oscillate(self.hass, _TEST_FAN, False) + common.oscillate(self.hass, _TEST_FAN, False) self.hass.block_till_done() # verify @@ -433,11 +434,11 @@ class TestTemplateFan: self._register_components() # Turn on fan - components.fan.turn_on(self.hass, _TEST_FAN) + common.turn_on(self.hass, _TEST_FAN) self.hass.block_till_done() # Set fan's osc to 'invalid' - components.fan.oscillate(self.hass, _TEST_FAN, 'invalid') + common.oscillate(self.hass, _TEST_FAN, 'invalid') self.hass.block_till_done() # verify @@ -449,11 +450,11 @@ class TestTemplateFan: self._register_components() # Turn on fan - components.fan.turn_on(self.hass, _TEST_FAN) + common.turn_on(self.hass, _TEST_FAN) self.hass.block_till_done() # Set fan's osc to True - components.fan.oscillate(self.hass, _TEST_FAN, True) + common.oscillate(self.hass, _TEST_FAN, True) self.hass.block_till_done() # verify @@ -461,7 +462,7 @@ class TestTemplateFan: self._verify(STATE_ON, None, True, None) # Set fan's osc to False - components.fan.oscillate(self.hass, _TEST_FAN, None) + common.oscillate(self.hass, _TEST_FAN, None) self.hass.block_till_done() # verify osc is unchanged @@ -473,11 +474,11 @@ class TestTemplateFan: self._register_components() # Turn on fan - components.fan.turn_on(self.hass, _TEST_FAN) + common.turn_on(self.hass, _TEST_FAN) self.hass.block_till_done() # Set fan's direction to forward - components.fan.set_direction(self.hass, _TEST_FAN, DIRECTION_FORWARD) + common.set_direction(self.hass, _TEST_FAN, DIRECTION_FORWARD) self.hass.block_till_done() # verify @@ -486,7 +487,7 @@ class TestTemplateFan: self._verify(STATE_ON, None, None, DIRECTION_FORWARD) # Set fan's direction to reverse - components.fan.set_direction(self.hass, _TEST_FAN, DIRECTION_REVERSE) + common.set_direction(self.hass, _TEST_FAN, DIRECTION_REVERSE) self.hass.block_till_done() # verify @@ -499,11 +500,11 @@ class TestTemplateFan: self._register_components() # Turn on fan - components.fan.turn_on(self.hass, _TEST_FAN) + common.turn_on(self.hass, _TEST_FAN) self.hass.block_till_done() # Set fan's direction to 'invalid' - components.fan.set_direction(self.hass, _TEST_FAN, 'invalid') + common.set_direction(self.hass, _TEST_FAN, 'invalid') self.hass.block_till_done() # verify direction is unchanged @@ -515,11 +516,11 @@ class TestTemplateFan: self._register_components() # Turn on fan - components.fan.turn_on(self.hass, _TEST_FAN) + common.turn_on(self.hass, _TEST_FAN) self.hass.block_till_done() # Set fan's direction to forward - components.fan.set_direction(self.hass, _TEST_FAN, DIRECTION_FORWARD) + common.set_direction(self.hass, _TEST_FAN, DIRECTION_FORWARD) self.hass.block_till_done() # verify @@ -528,7 +529,7 @@ class TestTemplateFan: self._verify(STATE_ON, None, None, DIRECTION_FORWARD) # Set fan's direction to 'invalid' - components.fan.set_direction(self.hass, _TEST_FAN, 'invalid') + common.set_direction(self.hass, _TEST_FAN, 'invalid') self.hass.block_till_done() # verify direction is unchanged From e2050926938707e05a48f7b07bb6436da66cf6cc Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 26 Sep 2018 10:15:49 +0200 Subject: [PATCH 065/247] Revert incorrect check (#16883) --- homeassistant/components/media_player/soundtouch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/media_player/soundtouch.py b/homeassistant/components/media_player/soundtouch.py index b8ade374a46..037a9b88fc6 100644 --- a/homeassistant/components/media_player/soundtouch.py +++ b/homeassistant/components/media_player/soundtouch.py @@ -297,7 +297,7 @@ class SoundTouchDevice(MediaPlayerDevice): def play_media(self, media_type, media_id, **kwargs): """Play a piece of media.""" _LOGGER.debug("Starting media with media_id: %s", media_id) - if re.match(r'https?://', str(media_id)): + if re.match(r'http?://', str(media_id)): # URL _LOGGER.debug("Playing URL %s", str(media_id)) self._device.play_url(str(media_id)) From e5861241c7ac6db4feb905be7e8f999d2840c02f Mon Sep 17 00:00:00 2001 From: Nicko van Someren Date: Wed, 26 Sep 2018 04:24:32 -0400 Subject: [PATCH 066/247] Added support for private storage. (#16878) * Addded support for private storage. Include 'private' flag parameters to the Store class and save_json function. Updated various authentication and onboarding classes to use private stores. Fixed unit test for emulated_hue which used a mock patch on save_json(). * Fixed Hound formatting issues not detected by local linting. --- homeassistant/auth/auth_store.py | 3 +- homeassistant/auth/mfa_modules/notify.py | 2 +- homeassistant/auth/mfa_modules/totp.py | 2 +- homeassistant/auth/providers/homeassistant.py | 3 +- .../components/onboarding/__init__.py | 3 +- homeassistant/helpers/storage.py | 5 +- homeassistant/util/json.py | 8 +- tests/components/emulated_hue/test_init.py | 91 ++++++++++--------- 8 files changed, 64 insertions(+), 53 deletions(-) diff --git a/homeassistant/auth/auth_store.py b/homeassistant/auth/auth_store.py index fb4700c806f..c170344746e 100644 --- a/homeassistant/auth/auth_store.py +++ b/homeassistant/auth/auth_store.py @@ -28,7 +28,8 @@ class AuthStore: """Initialize the auth store.""" self.hass = hass self._users = None # type: Optional[Dict[str, models.User]] - self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) + self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY, + private=True) async def async_get_users(self) -> List[models.User]: """Retrieve all users.""" diff --git a/homeassistant/auth/mfa_modules/notify.py b/homeassistant/auth/mfa_modules/notify.py index 84f9de614c1..03be4c74d32 100644 --- a/homeassistant/auth/mfa_modules/notify.py +++ b/homeassistant/auth/mfa_modules/notify.py @@ -85,7 +85,7 @@ class NotifyAuthModule(MultiFactorAuthModule): super().__init__(hass, config) self._user_settings = None # type: Optional[_UsersDict] self._user_store = hass.helpers.storage.Store( - STORAGE_VERSION, STORAGE_KEY) + STORAGE_VERSION, STORAGE_KEY, private=True) self._include = config.get(CONF_INCLUDE, []) self._exclude = config.get(CONF_EXCLUDE, []) self._message_template = config[CONF_MESSAGE] diff --git a/homeassistant/auth/mfa_modules/totp.py b/homeassistant/auth/mfa_modules/totp.py index 625cc0302e1..9b5896ef666 100644 --- a/homeassistant/auth/mfa_modules/totp.py +++ b/homeassistant/auth/mfa_modules/totp.py @@ -67,7 +67,7 @@ class TotpAuthModule(MultiFactorAuthModule): super().__init__(hass, config) self._users = None # type: Optional[Dict[str, str]] self._user_store = hass.helpers.storage.Store( - STORAGE_VERSION, STORAGE_KEY) + STORAGE_VERSION, STORAGE_KEY, private=True) @property def input_schema(self) -> vol.Schema: diff --git a/homeassistant/auth/providers/homeassistant.py b/homeassistant/auth/providers/homeassistant.py index c743a5b7f65..8710e7c60bc 100644 --- a/homeassistant/auth/providers/homeassistant.py +++ b/homeassistant/auth/providers/homeassistant.py @@ -52,7 +52,8 @@ class Data: def __init__(self, hass: HomeAssistant) -> None: """Initialize the user data store.""" self.hass = hass - self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) + self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY, + private=True) self._data = None # type: Optional[Dict[str, Any]] async def async_load(self) -> None: diff --git a/homeassistant/components/onboarding/__init__.py b/homeassistant/components/onboarding/__init__.py index 52d18b9a870..376575e3440 100644 --- a/homeassistant/components/onboarding/__init__.py +++ b/homeassistant/components/onboarding/__init__.py @@ -23,7 +23,8 @@ def async_is_onboarded(hass): async def async_setup(hass, config): """Set up the onboarding component.""" - store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) + store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY, + private=True) data = await store.async_load() if data is None: diff --git a/homeassistant/helpers/storage.py b/homeassistant/helpers/storage.py index 95e6925b2a4..7d867b50ec2 100644 --- a/homeassistant/helpers/storage.py +++ b/homeassistant/helpers/storage.py @@ -46,11 +46,12 @@ async def async_migrator(hass, old_path, store, *, class Store: """Class to help storing data.""" - def __init__(self, hass, version: int, key: str): + def __init__(self, hass, version: int, key: str, private: bool = False): """Initialize storage class.""" self.version = version self.key = key self.hass = hass + self._private = private self._data = None self._unsub_delay_listener = None self._unsub_stop_listener = None @@ -186,7 +187,7 @@ class Store: os.makedirs(os.path.dirname(path)) _LOGGER.debug('Writing data for %s', self.key) - json.save_json(path, data) + json.save_json(path, data, self._private) async def _async_migrate_func(self, old_version, old_data): """Migrate to the new version.""" diff --git a/homeassistant/util/json.py b/homeassistant/util/json.py index 8ecfebd5b33..40f25689148 100644 --- a/homeassistant/util/json.py +++ b/homeassistant/util/json.py @@ -3,6 +3,7 @@ import logging from typing import Union, List, Dict import json +import os from homeassistant.exceptions import HomeAssistantError @@ -38,14 +39,17 @@ def load_json(filename: str, default: Union[List, Dict, None] = None) \ return {} if default is None else default -def save_json(filename: str, data: Union[List, Dict]) -> None: +def save_json(filename: str, data: Union[List, Dict], + private: bool = False) -> None: """Save JSON data to a file. Returns True on success. """ try: json_data = json.dumps(data, sort_keys=True, indent=4) - with open(filename, 'w', encoding='utf-8') as fdesc: + mode = 0o600 if private else 0o644 + with open(os.open(filename, os.O_WRONLY | os.O_CREAT, mode), + 'w', encoding='utf-8') as fdesc: fdesc.write(json_data) except TypeError as error: _LOGGER.exception('Failed to serialize to JSON: %s', diff --git a/tests/components/emulated_hue/test_init.py b/tests/components/emulated_hue/test_init.py index 2f443eb5d6e..d416dd08e05 100644 --- a/tests/components/emulated_hue/test_init.py +++ b/tests/components/emulated_hue/test_init.py @@ -16,24 +16,25 @@ def test_config_google_home_entity_id_to_number(): handle = mop() with patch('homeassistant.util.json.open', mop, create=True): - number = conf.entity_id_to_number('light.test') - assert number == '2' - assert handle.write.call_count == 1 - assert json.loads(handle.write.mock_calls[0][1][0]) == { - '1': 'light.test2', - '2': 'light.test', - } + with patch('homeassistant.util.json.os.open', return_value=0): + number = conf.entity_id_to_number('light.test') + assert number == '2' + assert handle.write.call_count == 1 + assert json.loads(handle.write.mock_calls[0][1][0]) == { + '1': 'light.test2', + '2': 'light.test', + } - number = conf.entity_id_to_number('light.test') - assert number == '2' - assert handle.write.call_count == 1 + number = conf.entity_id_to_number('light.test') + assert number == '2' + assert handle.write.call_count == 1 - number = conf.entity_id_to_number('light.test2') - assert number == '1' - assert handle.write.call_count == 1 + number = conf.entity_id_to_number('light.test2') + assert number == '1' + assert handle.write.call_count == 1 - entity_id = conf.number_to_entity_id('1') - assert entity_id == 'light.test2' + entity_id = conf.number_to_entity_id('1') + assert entity_id == 'light.test2' def test_config_google_home_entity_id_to_number_altered(): @@ -46,24 +47,25 @@ def test_config_google_home_entity_id_to_number_altered(): handle = mop() with patch('homeassistant.util.json.open', mop, create=True): - number = conf.entity_id_to_number('light.test') - assert number == '22' - assert handle.write.call_count == 1 - assert json.loads(handle.write.mock_calls[0][1][0]) == { - '21': 'light.test2', - '22': 'light.test', - } + with patch('homeassistant.util.json.os.open', return_value=0): + number = conf.entity_id_to_number('light.test') + assert number == '22' + assert handle.write.call_count == 1 + assert json.loads(handle.write.mock_calls[0][1][0]) == { + '21': 'light.test2', + '22': 'light.test', + } - number = conf.entity_id_to_number('light.test') - assert number == '22' - assert handle.write.call_count == 1 + number = conf.entity_id_to_number('light.test') + assert number == '22' + assert handle.write.call_count == 1 - number = conf.entity_id_to_number('light.test2') - assert number == '21' - assert handle.write.call_count == 1 + number = conf.entity_id_to_number('light.test2') + assert number == '21' + assert handle.write.call_count == 1 - entity_id = conf.number_to_entity_id('21') - assert entity_id == 'light.test2' + entity_id = conf.number_to_entity_id('21') + assert entity_id == 'light.test2' def test_config_google_home_entity_id_to_number_empty(): @@ -76,23 +78,24 @@ def test_config_google_home_entity_id_to_number_empty(): handle = mop() with patch('homeassistant.util.json.open', mop, create=True): - number = conf.entity_id_to_number('light.test') - assert number == '1' - assert handle.write.call_count == 1 - assert json.loads(handle.write.mock_calls[0][1][0]) == { - '1': 'light.test', - } + with patch('homeassistant.util.json.os.open', return_value=0): + number = conf.entity_id_to_number('light.test') + assert number == '1' + assert handle.write.call_count == 1 + assert json.loads(handle.write.mock_calls[0][1][0]) == { + '1': 'light.test', + } - number = conf.entity_id_to_number('light.test') - assert number == '1' - assert handle.write.call_count == 1 + number = conf.entity_id_to_number('light.test') + assert number == '1' + assert handle.write.call_count == 1 - number = conf.entity_id_to_number('light.test2') - assert number == '2' - assert handle.write.call_count == 2 + number = conf.entity_id_to_number('light.test2') + assert number == '2' + assert handle.write.call_count == 2 - entity_id = conf.number_to_entity_id('2') - assert entity_id == 'light.test2' + entity_id = conf.number_to_entity_id('2') + assert entity_id == 'light.test2' def test_config_alexa_entity_id_to_number(): From 75c372021db70a3daf00719f2c5c1a5d64f07269 Mon Sep 17 00:00:00 2001 From: Jason Hu Date: Wed, 26 Sep 2018 02:20:48 -0700 Subject: [PATCH 067/247] Fix example for long-lived access token WS API (#16882) --- homeassistant/components/auth/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/auth/__init__.py b/homeassistant/components/auth/__init__.py index bee72d8e4fc..c0027fac820 100644 --- a/homeassistant/components/auth/__init__.py +++ b/homeassistant/components/auth/__init__.py @@ -105,7 +105,6 @@ Home Assistant. User need to record the token in secure place. "id": 11, "type": "auth/long_lived_access_token", "client_name": "GPS Logger", - "client_icon": null, "lifespan": 365 } From f13f723a045553fa05793eab054e04745d16d256 Mon Sep 17 00:00:00 2001 From: Blake Blackshear Date: Wed, 26 Sep 2018 04:57:16 -0500 Subject: [PATCH 068/247] Add bitwise operations as template helpers (#16833) --- homeassistant/helpers/template.py | 12 ++++++++++++ tests/helpers/test_template.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index d0d3fb457b1..c68aa311998 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -580,6 +580,16 @@ def regex_findall_index(value, find='', index=0, ignorecase=False): return re.findall(find, value, flags)[index] +def bitwise_and(first_value, second_value): + """Perform a bitwise and operation.""" + return first_value & second_value + + +def bitwise_or(first_value, second_value): + """Perform a bitwise or operation.""" + return first_value | second_value + + @contextfilter def random_every_time(context, values): """Choose a random value. @@ -617,6 +627,8 @@ ENV.filters['regex_match'] = regex_match ENV.filters['regex_replace'] = regex_replace ENV.filters['regex_search'] = regex_search ENV.filters['regex_findall_index'] = regex_findall_index +ENV.filters['bitwise_and'] = bitwise_and +ENV.filters['bitwise_or'] = bitwise_or ENV.globals['log'] = logarithm ENV.globals['sin'] = sine ENV.globals['cos'] = cosine diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index 6f426c290c5..dc8106e0ed3 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -562,6 +562,36 @@ class TestHelpersTemplate(unittest.TestCase): """, self.hass) self.assertEqual('LHR', tpl.render()) + def test_bitwise_and(self): + """Test bitwise_and method.""" + tpl = template.Template(""" +{{ 8 | bitwise_and(8) }} + """, self.hass) + self.assertEqual(str(8 & 8), tpl.render()) + tpl = template.Template(""" +{{ 10 | bitwise_and(2) }} + """, self.hass) + self.assertEqual(str(10 & 2), tpl.render()) + tpl = template.Template(""" +{{ 8 | bitwise_and(2) }} + """, self.hass) + self.assertEqual(str(8 & 2), tpl.render()) + + def test_bitwise_or(self): + """Test bitwise_or method.""" + tpl = template.Template(""" +{{ 8 | bitwise_or(8) }} + """, self.hass) + self.assertEqual(str(8 | 8), tpl.render()) + tpl = template.Template(""" +{{ 10 | bitwise_or(2) }} + """, self.hass) + self.assertEqual(str(10 | 2), tpl.render()) + tpl = template.Template(""" +{{ 8 | bitwise_or(2) }} + """, self.hass) + self.assertEqual(str(8 | 2), tpl.render()) + def test_distance_function_with_1_state(self): """Test distance function with 1 state.""" self.hass.states.set('test.object', 'happy', { From 917df1af007ab45a46d686797c053ba537e69884 Mon Sep 17 00:00:00 2001 From: Nikolay Vasilchuk Date: Wed, 26 Sep 2018 12:59:37 +0300 Subject: [PATCH 069/247] Telegram_bot polling support proxy_url and proxy_params (Fix #15746) (#16740) * Telegram bot polling proxy support * CI fix * houndci-bot review fix * houndci-bot review fix * CI fix * Review * Update polling.py --- .../components/telegram_bot/__init__.py | 5 +- .../components/telegram_bot/polling.py | 121 ++++++++---------- 2 files changed, 54 insertions(+), 72 deletions(-) diff --git a/homeassistant/components/telegram_bot/__init__.py b/homeassistant/components/telegram_bot/__init__.py index 8e24716ab57..75d9b611591 100644 --- a/homeassistant/components/telegram_bot/__init__.py +++ b/homeassistant/components/telegram_bot/__init__.py @@ -311,10 +311,11 @@ def initialize_bot(p_config): proxy_url = p_config.get(CONF_PROXY_URL) proxy_params = p_config.get(CONF_PROXY_PARAMS) - request = None if proxy_url is not None: - request = Request(proxy_url=proxy_url, + request = Request(con_pool_size=4, proxy_url=proxy_url, urllib3_proxy_kwargs=proxy_params) + else: + request = Request(con_pool_size=4) return Bot(token=api_key, request=request) diff --git a/homeassistant/components/telegram_bot/polling.py b/homeassistant/components/telegram_bot/polling.py index 6ee42b32504..0d4eddffd32 100644 --- a/homeassistant/components/telegram_bot/polling.py +++ b/homeassistant/components/telegram_bot/polling.py @@ -5,13 +5,8 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/telegram_bot.polling/ """ import asyncio -from asyncio.futures import CancelledError import logging -import async_timeout -from aiohttp.client_exceptions import ClientError -from aiohttp.hdrs import CONNECTION, KEEP_ALIVE - from homeassistant.components.telegram_bot import ( initialize_bot, CONF_ALLOWED_CHAT_IDS, BaseTelegramBotEntity, @@ -19,18 +14,10 @@ from homeassistant.components.telegram_bot import ( from homeassistant.const import ( EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) from homeassistant.core import callback -from homeassistant.helpers.aiohttp_client import async_get_clientsession _LOGGER = logging.getLogger(__name__) PLATFORM_SCHEMA = TELEGRAM_PLATFORM_SCHEMA -RETRY_SLEEP = 10 - - -class WrongHttpStatus(Exception): - """Thrown when a wrong http status is received.""" - - pass @asyncio.coroutine @@ -55,73 +42,67 @@ def async_setup_platform(hass, config): return True +def process_error(bot, update, error): + """Telegram bot error handler.""" + from telegram.error import ( + TelegramError, TimedOut, NetworkError, RetryAfter) + + try: + raise error + except (TimedOut, NetworkError, RetryAfter): + # Long polling timeout or connection problem. Nothing serious. + pass + except TelegramError: + _LOGGER.error('Update "%s" caused error "%s"', update, error) + + +def message_handler(handler): + """Create messages handler.""" + from telegram import Update + from telegram.ext import Handler + + class MessageHandler(Handler): + """Telegram bot message handler.""" + + def __init__(self): + """Initialize the messages handler instance.""" + super().__init__(handler) + + def check_update(self, update): + """Check is update valid.""" + return isinstance(update, Update) + + def handle_update(self, update, dispatcher): + """Handle update.""" + optional_args = self.collect_optional_args(dispatcher, update) + return self.callback(dispatcher.bot, update, **optional_args) + + return MessageHandler() + + class TelegramPoll(BaseTelegramBotEntity): """Asyncio telegram incoming message handler.""" def __init__(self, bot, hass, allowed_chat_ids): """Initialize the polling instance.""" + from telegram.ext import Updater + BaseTelegramBotEntity.__init__(self, hass, allowed_chat_ids) - self.update_id = 0 - self.websession = async_get_clientsession(hass) - self.update_url = '{0}/getUpdates'.format(bot.base_url) - self.polling_task = None # The actual polling task. - self.timeout = 15 # async post timeout - # Polling timeout should always be less than async post timeout. - self.post_data = {'timeout': self.timeout - 5} + + self.updater = Updater(bot=bot, workers=4) + self.dispatcher = self.updater.dispatcher + + self.dispatcher.add_handler(message_handler(self.process_update)) + self.dispatcher.add_error_handler(process_error) def start_polling(self): """Start the polling task.""" - self.polling_task = self.hass.async_add_job(self.check_incoming()) + self.updater.start_polling() def stop_polling(self): """Stop the polling task.""" - self.polling_task.cancel() + self.updater.stop() - @asyncio.coroutine - def get_updates(self, offset): - """Bypass the default long polling method to enable asyncio.""" - resp = None - if offset: - self.post_data['offset'] = offset - try: - with async_timeout.timeout(self.timeout, loop=self.hass.loop): - resp = yield from self.websession.post( - self.update_url, data=self.post_data, - headers={CONNECTION: KEEP_ALIVE} - ) - if resp.status == 200: - _json = yield from resp.json() - return _json - raise WrongHttpStatus('wrong status {}'.format(resp.status)) - finally: - if resp is not None: - yield from resp.release() - - @asyncio.coroutine - def check_incoming(self): - """Continuously check for incoming telegram messages.""" - try: - while True: - try: - _updates = yield from self.get_updates(self.update_id) - except (WrongHttpStatus, ClientError) as err: - # WrongHttpStatus: Non-200 status code. - # Occurs at times (mainly 502) and recovers - # automatically. Pause for a while before retrying. - _LOGGER.error(err) - yield from asyncio.sleep(RETRY_SLEEP) - except (asyncio.TimeoutError, ValueError): - # Long polling timeout. Nothing serious. - # Json error. Just retry for the next message. - pass - else: - # no exception raised. update received data. - _updates = _updates.get('result') - if _updates is None: - _LOGGER.error("Incorrect result received.") - else: - for update in _updates: - self.update_id = update['update_id'] + 1 - self.process_message(update) - except CancelledError: - _LOGGER.debug("Stopping Telegram polling bot") + def process_update(self, bot, update): + """Process incoming message.""" + self.process_message(update.to_dict()) From 98a4b1e9ac3ebad6c16752a22aafb82d4813fc70 Mon Sep 17 00:00:00 2001 From: cgtobi Date: Wed, 26 Sep 2018 13:07:43 +0200 Subject: [PATCH 070/247] Update language strings (#16884) --- homeassistant/components/auth/strings.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/auth/strings.json b/homeassistant/components/auth/strings.json index 2b1fc0c94f6..57f5ed659b0 100644 --- a/homeassistant/components/auth/strings.json +++ b/homeassistant/components/auth/strings.json @@ -17,15 +17,15 @@ "step": { "init": { "title": "Set up one-time password delivered by notify component", - "description": "Please select one of notify service:" + "description": "Please select one of the notification services:" }, "setup": { "title": "Verify setup", - "description": "A one-time password have sent by **notify.{notify_service}**. Please input it in below:" + "description": "A one-time password has been sent via **notify.{notify_service}**. Please enter it below:" } }, "abort": { - "no_available_service": "No available notify services." + "no_available_service": "No notification services available." }, "error": { "invalid_code": "Invalid code, please try again." From d20e0f5c951faca5ce0af0433dba4926baef3ec0 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 26 Sep 2018 11:19:34 +0200 Subject: [PATCH 071/247] Update frontend to 20180926.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 362c54fabbd..23ae6d485a6 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==20180924.0'] +REQUIREMENTS = ['home-assistant-frontend==20180926.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log', diff --git a/requirements_all.txt b/requirements_all.txt index 9f20ccb2ece..4600af0c542 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -454,7 +454,7 @@ hole==0.3.0 holidays==0.9.7 # homeassistant.components.frontend -home-assistant-frontend==20180924.0 +home-assistant-frontend==20180926.0 # homeassistant.components.homekit_controller # homekit==0.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 44baabc7f5d..691fc641607 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -87,7 +87,7 @@ hdate==0.6.3 holidays==0.9.7 # homeassistant.components.frontend -home-assistant-frontend==20180924.0 +home-assistant-frontend==20180926.0 # homeassistant.components.homematicip_cloud homematicip==0.9.8 From 732009c6689ff645e7a16717e6804e5b5a4374b5 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 26 Sep 2018 11:19:50 +0200 Subject: [PATCH 072/247] Update translations --- .../components/auth/.translations/ca.json | 21 ++++++++++- .../components/auth/.translations/de.json | 19 +++++++++- .../components/auth/.translations/fr.json | 7 ++++ .../components/auth/.translations/he.json | 35 +++++++++++++++++++ .../components/auth/.translations/ko.json | 19 ++++++++++ .../components/auth/.translations/lb.json | 19 ++++++++++ .../components/auth/.translations/pl.json | 18 ++++++++++ .../components/auth/.translations/ru.json | 19 ++++++++++ .../components/auth/.translations/sl.json | 19 ++++++++++ .../components/auth/.translations/sv.json | 14 ++++++++ .../auth/.translations/zh-Hans.json | 19 ++++++++++ .../auth/.translations/zh-Hant.json | 19 ++++++++++ .../components/cast/.translations/de.json | 2 +- .../components/deconz/.translations/he.json | 3 +- .../components/hangouts/.translations/he.json | 29 +++++++++++++++ .../components/hangouts/.translations/pl.json | 2 ++ .../homematicip_cloud/.translations/de.json | 10 +++--- .../homematicip_cloud/.translations/he.json | 1 + .../components/ios/.translations/he.json | 14 ++++++++ .../components/ios/.translations/sl.json | 14 ++++++++ .../components/ios/.translations/sv.json | 11 ++++++ .../components/mqtt/.translations/ca.json | 1 + .../components/mqtt/.translations/de.json | 6 ++++ .../components/mqtt/.translations/fr.json | 1 + .../components/mqtt/.translations/he.json | 24 +++++++++++++ .../components/mqtt/.translations/lb.json | 1 + .../components/mqtt/.translations/pl.json | 1 + .../components/mqtt/.translations/ru.json | 1 + .../components/mqtt/.translations/sl.json | 24 +++++++++++++ .../components/mqtt/.translations/sv.json | 13 +++++++ .../mqtt/.translations/zh-Hans.json | 1 + .../mqtt/.translations/zh-Hant.json | 1 + .../components/nest/.translations/de.json | 2 +- .../components/openuv/.translations/de.json | 3 +- .../components/openuv/.translations/he.json | 20 +++++++++++ .../sensor/.translations/moon.he.json | 6 +++- .../components/sonos/.translations/de.json | 2 +- .../components/tradfri/.translations/de.json | 22 ++++++++++++ .../components/tradfri/.translations/he.json | 23 ++++++++++++ .../components/tradfri/.translations/pl.json | 22 ++++++++++++ .../components/tradfri/.translations/sl.json | 23 ++++++++++++ .../components/tradfri/.translations/sv.json | 20 +++++++++++ .../tradfri/.translations/zh-Hans.json | 13 ++++++- 43 files changed, 530 insertions(+), 14 deletions(-) create mode 100644 homeassistant/components/auth/.translations/he.json create mode 100644 homeassistant/components/hangouts/.translations/he.json create mode 100644 homeassistant/components/ios/.translations/he.json create mode 100644 homeassistant/components/ios/.translations/sl.json create mode 100644 homeassistant/components/ios/.translations/sv.json create mode 100644 homeassistant/components/mqtt/.translations/he.json create mode 100644 homeassistant/components/mqtt/.translations/sl.json create mode 100644 homeassistant/components/mqtt/.translations/sv.json create mode 100644 homeassistant/components/openuv/.translations/he.json create mode 100644 homeassistant/components/tradfri/.translations/de.json create mode 100644 homeassistant/components/tradfri/.translations/he.json create mode 100644 homeassistant/components/tradfri/.translations/pl.json create mode 100644 homeassistant/components/tradfri/.translations/sl.json create mode 100644 homeassistant/components/tradfri/.translations/sv.json diff --git a/homeassistant/components/auth/.translations/ca.json b/homeassistant/components/auth/.translations/ca.json index 1b3b25dbcff..f4318a0eb21 100644 --- a/homeassistant/components/auth/.translations/ca.json +++ b/homeassistant/components/auth/.translations/ca.json @@ -1,8 +1,27 @@ { "mfa_setup": { + "notify": { + "abort": { + "no_available_service": "No hi ha serveis de notificaci\u00f3 disponibles." + }, + "error": { + "invalid_code": "Codi inv\u00e0lid, si us plau torni a provar-ho." + }, + "step": { + "init": { + "description": "Seleccioneu un dels serveis de notificaci\u00f3:", + "title": "Configureu una contrasenya d'un sol \u00fas a trav\u00e9s del component de notificacions" + }, + "setup": { + "description": "**notify.{notify_service}** ha enviat una contrasenya d'un sol \u00fas. Introdu\u00efu-la a continuaci\u00f3:", + "title": "Verifiqueu la configuraci\u00f3" + } + }, + "title": "Contrasenya d'un sol \u00fas del servei de notificacions" + }, "totp": { "error": { - "invalid_code": "Codi no v\u00e0lid, si us plau torni a provar-ho. Si obteniu aquest error repetidament, assegureu-vos que la data i hora de Home Assistant sigui correcta i precisa." + "invalid_code": "Codi inv\u00e0lid, si us plau torni a provar-ho. Si obteniu aquest error repetidament, assegureu-vos que la data i hora de Home Assistant sigui correcta i precisa." }, "step": { "init": { diff --git a/homeassistant/components/auth/.translations/de.json b/homeassistant/components/auth/.translations/de.json index 67f948e8340..2abc64f5f5d 100644 --- a/homeassistant/components/auth/.translations/de.json +++ b/homeassistant/components/auth/.translations/de.json @@ -1,8 +1,25 @@ { "mfa_setup": { + "notify": { + "abort": { + "no_available_service": "Keine Benachrichtigungsdienste verf\u00fcgbar." + }, + "error": { + "invalid_code": "Ung\u00fcltiger Code, bitte versuche es erneut." + }, + "step": { + "init": { + "description": "Bitte w\u00e4hle einen der Benachrichtigungsdienste:" + }, + "setup": { + "description": "Ein Einmal-Passwort wurde per ** notify gesendet. {notify_service} **. Bitte gebe es unten ein:", + "title": "\u00dcberpr\u00fcfe das Setup" + } + } + }, "totp": { "error": { - "invalid_code": "Ung\u00fcltiger Code, bitte versuche es erneut. Wenn Sie diesen Fehler regelm\u00e4\u00dfig erhalten, stelle sicher, dass die Uhr deines Home Assistant-Systems korrekt ist." + "invalid_code": "Ung\u00fcltiger Code, bitte versuche es erneut. Wenn du diesen Fehler regelm\u00e4\u00dfig erhalten, stelle sicher, dass die Uhr deines Home Assistant-Systems korrekt ist." }, "step": { "init": { diff --git a/homeassistant/components/auth/.translations/fr.json b/homeassistant/components/auth/.translations/fr.json index b8d10dc89d0..85540314af0 100644 --- a/homeassistant/components/auth/.translations/fr.json +++ b/homeassistant/components/auth/.translations/fr.json @@ -1,5 +1,12 @@ { "mfa_setup": { + "notify": { + "step": { + "setup": { + "description": "Un mot de passe unique a \u00e9t\u00e9 envoy\u00e9 par **notify.{notify_service}**. Veuillez le saisir ci-dessous :" + } + } + }, "totp": { "error": { "invalid_code": "Code invalide. Veuillez essayez \u00e0 nouveau. Si cette erreur persiste, assurez-vous que l'horloge de votre syst\u00e8me Home Assistant est correcte." diff --git a/homeassistant/components/auth/.translations/he.json b/homeassistant/components/auth/.translations/he.json new file mode 100644 index 00000000000..bc1826d4d79 --- /dev/null +++ b/homeassistant/components/auth/.translations/he.json @@ -0,0 +1,35 @@ +{ + "mfa_setup": { + "notify": { + "abort": { + "no_available_service": "\u05d0\u05d9\u05df \u05e9\u05d9\u05e8\u05d5\u05ea\u05d9 notify \u05d6\u05de\u05d9\u05e0\u05d9\u05dd." + }, + "error": { + "invalid_code": "\u05e7\u05d5\u05d3 \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9, \u05d0\u05e0\u05d0 \u05e0\u05e1\u05d4 \u05e9\u05d5\u05d1." + }, + "step": { + "init": { + "description": "\u05d1\u05d7\u05e8 \u05d0\u05ea \u05d0\u05d7\u05d3 \u05de\u05e9\u05e8\u05d5\u05ea\u05d9 notify", + "title": "\u05d4\u05d2\u05d3\u05e8 \u05e1\u05d9\u05e1\u05de\u05d4 \u05d7\u05d3 \u05e4\u05e2\u05de\u05d9\u05ea \u05d4\u05e0\u05e9\u05dc\u05d7\u05ea \u05e2\u05dc \u05d9\u05d3\u05d9 \u05e8\u05db\u05d9\u05d1 notify" + }, + "setup": { + "description": "\u05e1\u05d9\u05e1\u05de\u05d4 \u05d7\u05d3 \u05e4\u05e2\u05de\u05d9\u05ea \u05e0\u05e9\u05dc\u05d7\u05d4 \u05e2\u05dc \u05d9\u05d3\u05d9 **{notify_service}**. \u05d4\u05d6\u05df \u05d0\u05d5\u05ea\u05d4 \u05dc\u05de\u05d8\u05d4:", + "title": "\u05d0\u05d9\u05de\u05d5\u05ea \u05d4\u05d4\u05ea\u05e7\u05e0\u05d4" + } + }, + "title": "\u05dc\u05d4\u05d5\u05d3\u05d9\u05e2 \u200b\u200b\u05e2\u05dc \u05e1\u05d9\u05e1\u05de\u05d4 \u05d7\u05d3 \u05e4\u05e2\u05de\u05d9\u05ea" + }, + "totp": { + "error": { + "invalid_code": "\u05e7\u05d5\u05d3 \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9, \u05d0\u05e0\u05d0 \u05e0\u05e1\u05d4 \u05e9\u05d5\u05d1. \u05d0\u05dd \u05d0\u05ea\u05d4 \u05de\u05e7\u05d1\u05dc \u05d0\u05ea \u05d4\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d4\u05d6\u05d5 \u05d1\u05d0\u05d5\u05e4\u05df \u05e2\u05e7\u05d1\u05d9, \u05d5\u05d3\u05d0 \u05e9\u05d4\u05e9\u05e2\u05d5\u05df \u05e9\u05dc \u05de\u05e2\u05e8\u05db\u05ea \u05d4 - Home Assistant \u05e9\u05dc\u05da \u05de\u05d3\u05d5\u05d9\u05e7." + }, + "step": { + "init": { + "description": "\u05db\u05d3\u05d9 \u05dc\u05d4\u05e4\u05e2\u05d9\u05dc \u05d0\u05d9\u05de\u05d5\u05ea \u05d3\u05d5 \u05e9\u05dc\u05d1\u05d9 \u05d1\u05d0\u05de\u05e6\u05e2\u05d5\u05ea \u05e1\u05d9\u05e1\u05de\u05d0\u05d5\u05ea \u05d7\u05d3 \u05e4\u05e2\u05de\u05d9\u05d5\u05ea \u05de\u05d1\u05d5\u05e1\u05e1\u05d5\u05ea \u05d6\u05de\u05df, \u05e1\u05e8\u05d5\u05e7 \u05d0\u05ea \u05e7\u05d5\u05d3 QR \u05e2\u05dd \u05d9\u05d9\u05e9\u05d5\u05dd \u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05e9\u05dc\u05da. \u05d0\u05dd \u05d0\u05d9\u05df \u05dc\u05da \u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d6\u05d4, \u05d0\u05e0\u05d5 \u05de\u05de\u05dc\u05d9\u05e6\u05d9\u05dd \u05e2\u05dc [Google Authenticator] (https://support.google.com/accounts/answer/1066447) \u05d0\u05d5 [Authy] (https://authy.com/). \n\n {qr_code} \n \n \u05dc\u05d0\u05d7\u05e8 \u05e1\u05e8\u05d9\u05e7\u05ea \u05d4\u05e7\u05d5\u05d3, \u05d4\u05d6\u05df \u05d0\u05ea \u05d4\u05e7\u05d5\u05d3 \u05d1\u05df \u05e9\u05e9 \u05d4\u05e1\u05e4\u05e8\u05d5\u05ea \u05de\u05d4\u05d0\u05e4\u05dc\u05d9\u05e7\u05e6\u05d9\u05d4 \u05e9\u05dc\u05da \u05db\u05d3\u05d9 \u05dc\u05d0\u05de\u05ea \u05d0\u05ea \u05d4\u05d4\u05d2\u05d3\u05e8\u05d4. \u05d0\u05dd \u05d0\u05ea\u05d4 \u05e0\u05ea\u05e7\u05dc \u05d1\u05d1\u05e2\u05d9\u05d5\u05ea \u05d1\u05e1\u05e8\u05d9\u05e7\u05ea \u05e7\u05d5\u05d3 QR, \u05d1\u05e6\u05e2 \u05d4\u05d2\u05d3\u05e8\u05d4 \u05d9\u05d3\u05e0\u05d9\u05ea \u05e2\u05dd \u05e7\u05d5\u05d3 **`{code}`**.", + "title": "\u05d4\u05d2\u05d3\u05e8 \u05d0\u05d9\u05de\u05d5\u05ea \u05d3\u05d5 \u05e9\u05dc\u05d1\u05d9 \u05d1\u05d0\u05de\u05e6\u05e2\u05d5\u05ea TOTP" + } + }, + "title": "TOTP" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/auth/.translations/ko.json b/homeassistant/components/auth/.translations/ko.json index 17fb5c56f57..e1f26e88bc7 100644 --- a/homeassistant/components/auth/.translations/ko.json +++ b/homeassistant/components/auth/.translations/ko.json @@ -1,5 +1,24 @@ { "mfa_setup": { + "notify": { + "abort": { + "no_available_service": "\uc0ac\uc6a9 \uac00\ub2a5\ud55c \uc54c\ub9bc \uc11c\ube44\uc2a4\uac00 \uc5c6\uc2b5\ub2c8\ub2e4." + }, + "error": { + "invalid_code": "\uc798\ubabb\ub41c \ucf54\ub4dc\uc785\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694." + }, + "step": { + "init": { + "description": "\uc54c\ub9bc \uc11c\ube44\uc2a4 \uc911 \ud558\ub098\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694:", + "title": "\uc54c\ub9bc \uad6c\uc131\uc694\uc18c\uac00 \uc81c\uacf5\ud558\ub294 \uc77c\ud68c\uc6a9 \ube44\ubc00\ubc88\ud638 \uc124\uc815" + }, + "setup": { + "description": "**notify.{notify_service}** \uc5d0\uc11c \uc77c\ud68c\uc6a9 \ube44\ubc00\ubc88\ud638\ub97c \ubcf4\ub0c8\uc2b5\ub2c8\ub2e4. \uc544\ub798\uc758 \uacf5\ub780\uc5d0 \uc785\ub825\ud574 \uc8fc\uc138\uc694:", + "title": "\uc124\uc815 \ud655\uc778" + } + }, + "title": "\uc77c\ud68c\uc6a9 \ube44\ubc00\ubc88\ud638 \uc54c\ub9bc" + }, "totp": { "error": { "invalid_code": "\uc798\ubabb\ub41c \ucf54\ub4dc \uc785\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694. \uc774 \uc624\ub958\uac00 \uc9c0\uc18d\uc801\uc73c\ub85c \ubc1c\uc0dd\ud55c\ub2e4\uba74 Home Assistant \uc758 \uc2dc\uac04\uc124\uc815\uc774 \uc62c\ubc14\ub978\uc9c0 \ud655\uc778\ud574\ubcf4\uc138\uc694." diff --git a/homeassistant/components/auth/.translations/lb.json b/homeassistant/components/auth/.translations/lb.json index f55ae4b97ba..12ced930446 100644 --- a/homeassistant/components/auth/.translations/lb.json +++ b/homeassistant/components/auth/.translations/lb.json @@ -1,5 +1,24 @@ { "mfa_setup": { + "notify": { + "abort": { + "no_available_service": "Keen Notifikatioun's D\u00e9ngscht disponibel." + }, + "error": { + "invalid_code": "Ong\u00ebltege Code, prob\u00e9iert w.e.g. nach emol." + }, + "step": { + "init": { + "description": "Wielt w.e.g. een Notifikatioun's D\u00e9ngscht aus:", + "title": "Eemolegt Passwuert ariichte wat vun engem Notifikatioun's Komponente versch\u00e9ckt g\u00ebtt" + }, + "setup": { + "description": "Een eemolegt Passwuert ass vun **notify.{notify_service}** gesch\u00e9ckt ginn. Gitt et w.e.g hei \u00ebnnen dr\u00ebnner an:", + "title": "Astellungen iwwerpr\u00e9iwen" + } + }, + "title": "Eemolegt Passwuert Notifikatioun" + }, "totp": { "error": { "invalid_code": "Ong\u00ebltege Login, prob\u00e9iert w.e.g. nach emol. Falls d\u00ebse Feeler Message \u00ebmmer er\u00ebm optr\u00ebtt dann iwwerpr\u00e9ift op d'Z\u00e4it vum Home Assistant System richteg ass." diff --git a/homeassistant/components/auth/.translations/pl.json b/homeassistant/components/auth/.translations/pl.json index 78999c34c22..3e320ba8d62 100644 --- a/homeassistant/components/auth/.translations/pl.json +++ b/homeassistant/components/auth/.translations/pl.json @@ -1,5 +1,23 @@ { "mfa_setup": { + "notify": { + "abort": { + "no_available_service": "Brak dost\u0119pnych us\u0142ug powiadamiania." + }, + "error": { + "invalid_code": "Nieprawid\u0142owy kod, spr\u00f3buj ponownie." + }, + "step": { + "init": { + "description": "Prosz\u0119 wybra\u0107 jedn\u0105 z us\u0142ugi powiadamiania:", + "title": "Skonfiguruj has\u0142o jednorazowe dostarczone przez komponent powiadomie\u0144" + }, + "setup": { + "description": "Has\u0142o jednorazowe zosta\u0142o wys\u0142ane przez ** powiadom. {notify_service} **. Wpisz je poni\u017cej:", + "title": "Sprawd\u017a konfiguracj\u0119" + } + } + }, "totp": { "error": { "invalid_code": "Nieprawid\u0142owy kod, spr\u00f3buj ponownie. Je\u015bli b\u0142\u0105d b\u0119dzie si\u0119 powtarza\u0142, upewnij si\u0119, \u017ce czas zegara systemu Home Assistant jest prawid\u0142owy." diff --git a/homeassistant/components/auth/.translations/ru.json b/homeassistant/components/auth/.translations/ru.json index a716425f345..edf136bd7f3 100644 --- a/homeassistant/components/auth/.translations/ru.json +++ b/homeassistant/components/auth/.translations/ru.json @@ -1,5 +1,24 @@ { "mfa_setup": { + "notify": { + "abort": { + "no_available_service": "\u041d\u0435\u0442 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u044b\u0445 \u0441\u043b\u0443\u0436\u0431 \u0443\u0432\u0435\u0434\u043e\u043c\u043b\u0435\u043d\u0438\u0439." + }, + "error": { + "invalid_code": "\u041d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u044b\u0439 \u043a\u043e\u0434, \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443." + }, + "step": { + "init": { + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043e\u0434\u043d\u0443 \u0438\u0437 \u0441\u043b\u0443\u0436\u0431 \u0443\u0432\u0435\u0434\u043e\u043c\u043b\u0435\u043d\u0438\u0439:", + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0434\u043e\u0441\u0442\u0430\u0432\u043a\u0438 \u043e\u0434\u043d\u043e\u0440\u0430\u0437\u043e\u0432\u044b\u0445 \u043f\u0430\u0440\u043e\u043b\u0435\u0439 \u0447\u0435\u0440\u0435\u0437 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u0443\u0432\u0435\u0434\u043e\u043c\u043b\u0435\u043d\u0438\u0439" + }, + "setup": { + "description": "\u041e\u0434\u043d\u043e\u0440\u0430\u0437\u043e\u0432\u044b\u0439 \u043f\u0430\u0440\u043e\u043b\u044c \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u0435\u043d \u0447\u0435\u0440\u0435\u0437 **notify.{notify_service}**. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0432\u0432\u0435\u0434\u0438\u0442\u0435 \u0435\u0433\u043e \u043d\u0438\u0436\u0435:", + "title": "\u041f\u0440\u043e\u0432\u0435\u0440\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443" + } + }, + "title": "\u0414\u043e\u0441\u0442\u0430\u0432\u043a\u0430 \u043e\u0434\u043d\u043e\u0440\u0430\u0437\u043e\u0432\u044b\u0445 \u043f\u0430\u0440\u043e\u043b\u0435\u0439" + }, "totp": { "error": { "invalid_code": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043e\u0434. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0441\u043d\u043e\u0432\u0430. \u0415\u0441\u043b\u0438 \u0432\u044b \u043f\u043e\u0441\u0442\u043e\u044f\u043d\u043d\u043e \u043f\u043e\u043b\u0443\u0447\u0430\u0435\u0442\u0435 \u044d\u0442\u0443 \u043e\u0448\u0438\u0431\u043a\u0443, \u043f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0443\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e \u0447\u0430\u0441\u044b \u0432 \u0432\u0430\u0448\u0435\u0439 \u0441\u0438\u0441\u0442\u0435\u043c\u0435 Home Assistant \u043f\u043e\u043a\u0430\u0437\u044b\u0432\u0430\u044e\u0442 \u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u043e\u0435 \u0432\u0440\u0435\u043c\u044f." diff --git a/homeassistant/components/auth/.translations/sl.json b/homeassistant/components/auth/.translations/sl.json index 45b57a772f9..2efc23f78f6 100644 --- a/homeassistant/components/auth/.translations/sl.json +++ b/homeassistant/components/auth/.translations/sl.json @@ -1,5 +1,24 @@ { "mfa_setup": { + "notify": { + "abort": { + "no_available_service": "Ni na voljo storitev obve\u0161\u010danja." + }, + "error": { + "invalid_code": "Neveljavna koda, poskusite znova." + }, + "step": { + "init": { + "description": "Prosimo, izberite eno od storitev obve\u0161\u010danja:", + "title": "Nastavite enkratno geslo, ki ga dostavite z obvestilno komponento" + }, + "setup": { + "description": "Enkratno geslo je poslal **notify.{notify_service} **. Vnesite ga spodaj:", + "title": "Preverite nastavitev" + } + }, + "title": "Obvesti Enkratno Geslo" + }, "totp": { "error": { "invalid_code": "Neveljavna koda, prosimo, poskusite znova. \u010ce dobite to sporo\u010dilo ve\u010dkrat, prosimo poskrbite, da bo ura va\u0161ega Home Assistenta to\u010dna." diff --git a/homeassistant/components/auth/.translations/sv.json b/homeassistant/components/auth/.translations/sv.json index cf8227c09a3..604ae3c4fe5 100644 --- a/homeassistant/components/auth/.translations/sv.json +++ b/homeassistant/components/auth/.translations/sv.json @@ -1,5 +1,19 @@ { "mfa_setup": { + "notify": { + "abort": { + "no_available_service": "Inga tillg\u00e4ngliga meddelande tj\u00e4nster." + }, + "error": { + "invalid_code": "Ogiltig kod, var god f\u00f6rs\u00f6k igen." + }, + "step": { + "setup": { + "description": "Ett eng\u00e5ngsl\u00f6senord har skickats av **notify.{notify_service}**. V\u00e4nligen ange det nedan:", + "title": "Verifiera installationen" + } + } + }, "totp": { "error": { "invalid_code": "Ogiltig kod, f\u00f6rs\u00f6k igen. Om du flera g\u00e5nger i rad f\u00e5r detta fel, se till att klockan i din Home Assistant \u00e4r korrekt inst\u00e4lld." diff --git a/homeassistant/components/auth/.translations/zh-Hans.json b/homeassistant/components/auth/.translations/zh-Hans.json index c5b397a8e12..d2a1b97b9b7 100644 --- a/homeassistant/components/auth/.translations/zh-Hans.json +++ b/homeassistant/components/auth/.translations/zh-Hans.json @@ -1,5 +1,24 @@ { "mfa_setup": { + "notify": { + "abort": { + "no_available_service": "\u6ca1\u6709\u53ef\u7528\u7684\u901a\u77e5\u670d\u52a1\u3002" + }, + "error": { + "invalid_code": "\u4ee3\u7801\u65e0\u6548\uff0c\u8bf7\u518d\u8bd5\u4e00\u6b21\u3002" + }, + "step": { + "init": { + "description": "\u8bf7\u9009\u62e9\u4ee5\u4e0b\u4e00\u4e2a\u901a\u77e5\u670d\u52a1\uff1a", + "title": "\u8bbe\u7f6e\u7531\u901a\u77e5\u7ec4\u4ef6\u4f20\u9012\u7684\u4e00\u6b21\u6027\u5bc6\u7801" + }, + "setup": { + "description": "\u4e00\u6b21\u6027\u5bc6\u7801\u5df2\u7531 **notify.{notify_service}**\u53d1\u9001\u3002\u8bf7\u5728\u4e0b\u9762\u8f93\u5165:", + "title": "\u9a8c\u8bc1\u8bbe\u7f6e" + } + }, + "title": "\u4e00\u6b21\u6027\u5bc6\u7801\u901a\u77e5" + }, "totp": { "error": { "invalid_code": "\u53e3\u4ee4\u65e0\u6548\uff0c\u8bf7\u91cd\u65b0\u8f93\u5165\u3002\u5982\u679c\u9519\u8bef\u53cd\u590d\u51fa\u73b0\uff0c\u8bf7\u786e\u4fdd Home Assistant \u7cfb\u7edf\u7684\u65f6\u95f4\u51c6\u786e\u65e0\u8bef\u3002" diff --git a/homeassistant/components/auth/.translations/zh-Hant.json b/homeassistant/components/auth/.translations/zh-Hant.json index ef41ea87248..e791f20a738 100644 --- a/homeassistant/components/auth/.translations/zh-Hant.json +++ b/homeassistant/components/auth/.translations/zh-Hant.json @@ -1,5 +1,24 @@ { "mfa_setup": { + "notify": { + "abort": { + "no_available_service": "\u6c92\u6709\u53ef\u7528\u7684\u901a\u77e5\u670d\u52d9\u3002" + }, + "error": { + "invalid_code": "\u9a57\u8b49\u78bc\u7121\u6548\uff0c\u8acb\u518d\u8a66\u4e00\u6b21\u3002" + }, + "step": { + "init": { + "description": "\u8acb\u9078\u64c7\u4e00\u9805\u901a\u77e5\u670d\u52d9\uff1a", + "title": "\u8a2d\u5b9a\u4e00\u6b21\u6027\u5bc6\u78bc\u50b3\u9001\u7d44\u4ef6" + }, + "setup": { + "description": "\u4e00\u6b21\u6027\u5bc6\u78bc\u5df2\u900f\u904e **notify.{notify_service}** \u50b3\u9001\u3002\u8acb\u65bc\u4e0b\u65b9\u8f38\u5165\uff1a", + "title": "\u9a57\u8b49\u8a2d\u5b9a" + } + }, + "title": "\u901a\u77e5\u4e00\u6b21\u6027\u5bc6\u78bc" + }, "totp": { "error": { "invalid_code": "\u9a57\u8b49\u78bc\u7121\u6548\uff0c\u8acb\u518d\u8a66\u4e00\u6b21\u3002\u5047\u5982\u932f\u8aa4\u6301\u7e8c\u767c\u751f\uff0c\u8acb\u5148\u78ba\u5b9a\u60a8\u7684 Home Assistant \u7cfb\u7d71\u4e0a\u7684\u6642\u9593\u8a2d\u5b9a\u6b63\u78ba\u5f8c\uff0c\u518d\u8a66\u4e00\u6b21\u3002" diff --git a/homeassistant/components/cast/.translations/de.json b/homeassistant/components/cast/.translations/de.json index a37dbd6f5b7..ac1ebbeb236 100644 --- a/homeassistant/components/cast/.translations/de.json +++ b/homeassistant/components/cast/.translations/de.json @@ -6,7 +6,7 @@ }, "step": { "confirm": { - "description": "M\u00f6chten Sie Google Cast einrichten?", + "description": "M\u00f6chtest du Google Cast einrichten?", "title": "Google Cast" } }, diff --git a/homeassistant/components/deconz/.translations/he.json b/homeassistant/components/deconz/.translations/he.json index b4b3d54e075..89a2d69950e 100644 --- a/homeassistant/components/deconz/.translations/he.json +++ b/homeassistant/components/deconz/.translations/he.json @@ -2,7 +2,8 @@ "config": { "abort": { "already_configured": "\u05d4\u05de\u05d2\u05e9\u05e8 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8", - "no_bridges": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05d2\u05e9\u05e8\u05d9 deCONZ" + "no_bridges": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05d2\u05e9\u05e8\u05d9 deCONZ", + "one_instance_only": "\u05d4\u05e8\u05db\u05d9\u05d1 \u05ea\u05d5\u05de\u05da \u05e8\u05e7 \u05d0\u05d7\u05d3 deCONZ \u05dc\u05de\u05e9\u05dc" }, "error": { "no_key": "\u05dc\u05d0 \u05e0\u05d9\u05ea\u05df \u05d4\u05d9\u05d4 \u05dc\u05e7\u05d1\u05dc \u05de\u05e4\u05ea\u05d7 API" diff --git a/homeassistant/components/hangouts/.translations/he.json b/homeassistant/components/hangouts/.translations/he.json new file mode 100644 index 00000000000..28326d97142 --- /dev/null +++ b/homeassistant/components/hangouts/.translations/he.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Google Hangouts \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8", + "unknown": "\u05d0\u05d9\u05e8\u05e2\u05d4 \u05e9\u05d2\u05d9\u05d0\u05d4 \u05dc\u05d0 \u05d9\u05d3\u05d5\u05e2\u05d4." + }, + "error": { + "invalid_2fa": "\u05d0\u05d9\u05de\u05d5\u05ea \u05d3\u05d5 \u05e9\u05dc\u05d1\u05d9 \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9, \u05d1\u05d1\u05e7\u05e9\u05d4 \u05e0\u05e1\u05d4 \u05e9\u05d5\u05d1.", + "invalid_2fa_method": "\u05d3\u05e8\u05da \u05dc\u05d0\u05d9\u05de\u05d5\u05ea \u05d3\u05d5 \u05e9\u05dc\u05d1\u05d9 \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9\u05ea (\u05d0\u05de\u05ea \u05d1\u05d8\u05dc\u05e4\u05d5\u05df).", + "invalid_login": "\u05db\u05e0\u05d9\u05e1\u05d4 \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9\u05ea, \u05e0\u05e1\u05d4 \u05e9\u05d5\u05d1." + }, + "step": { + "2fa": { + "data": { + "2fa": "\u05e7\u05d5\u05d3 \u05d0\u05d9\u05de\u05d5\u05ea \u05d3\u05d5 \u05e9\u05dc\u05d1\u05d9" + }, + "title": "\u05d0\u05d9\u05de\u05d5\u05ea \u05d3\u05d5 \u05e9\u05dc\u05d1\u05d9" + }, + "user": { + "data": { + "email": "\u05db\u05ea\u05d5\u05d1\u05ea \u05d3\u05d5\u05d0\"\u05dc", + "password": "\u05e1\u05d9\u05e1\u05de\u05d4" + }, + "title": "\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05dc- Google Hangouts" + } + }, + "title": "Google Hangouts" + } +} \ No newline at end of file diff --git a/homeassistant/components/hangouts/.translations/pl.json b/homeassistant/components/hangouts/.translations/pl.json index a8314761f8d..5e0ecfa2900 100644 --- a/homeassistant/components/hangouts/.translations/pl.json +++ b/homeassistant/components/hangouts/.translations/pl.json @@ -14,6 +14,7 @@ "data": { "2fa": "PIN" }, + "description": "Pusty", "title": "Uwierzytelnianie dwusk\u0142adnikowe" }, "user": { @@ -21,6 +22,7 @@ "email": "Adres e-mail", "password": "Has\u0142o" }, + "description": "Pusty", "title": "Logowanie do Google Hangouts" } }, diff --git a/homeassistant/components/homematicip_cloud/.translations/de.json b/homeassistant/components/homematicip_cloud/.translations/de.json index 6b3b9b09480..bd600f7d2ef 100644 --- a/homeassistant/components/homematicip_cloud/.translations/de.json +++ b/homeassistant/components/homematicip_cloud/.translations/de.json @@ -6,10 +6,10 @@ "unknown": "Ein unbekannter Fehler ist aufgetreten." }, "error": { - "invalid_pin": "Ung\u00fcltige PIN, bitte versuchen Sie es erneut.", - "press_the_button": "Bitte dr\u00fccken Sie die blaue Taste.", - "register_failed": "Registrierung fehlgeschlagen, bitte versuchen Sie es erneut.", - "timeout_button": "Zeit\u00fcberschreitung beim Dr\u00fccken der blauen Taste. Bitte versuchen Sie es erneut." + "invalid_pin": "Ung\u00fcltige PIN, bitte versuche es erneut.", + "press_the_button": "Bitte dr\u00fccke die blaue Taste.", + "register_failed": "Registrierung fehlgeschlagen, bitte versuche es erneut.", + "timeout_button": "Zeit\u00fcberschreitung beim Dr\u00fccken der blauen Taste. Bitte versuche es erneut." }, "step": { "init": { @@ -21,7 +21,7 @@ "title": "HometicIP Accesspoint ausw\u00e4hlen" }, "link": { - "description": "Dr\u00fccken Sie den blauen Taster auf dem Accesspoint, sowie den Senden Button um HomematicIP mit Home Assistant zu verbinden.\n\n![Position des Tasters auf dem AP](/static/images/config_flows/config_homematicip_cloud.png)", + "description": "Dr\u00fccke den blauen Taster auf dem Accesspoint, sowie den Senden Button um HomematicIP mit Home Assistant zu verbinden.\n\n![Position des Tasters auf dem AP](/static/images/config_flows/config_homematicip_cloud.png)", "title": "Verkn\u00fcpfe den Accesspoint" } }, diff --git a/homeassistant/components/homematicip_cloud/.translations/he.json b/homeassistant/components/homematicip_cloud/.translations/he.json index 50d5c61b576..c60294e21d5 100644 --- a/homeassistant/components/homematicip_cloud/.translations/he.json +++ b/homeassistant/components/homematicip_cloud/.translations/he.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "\u05e0\u05e7\u05d5\u05d3\u05ea \u05d4\u05d2\u05d9\u05e9\u05d4 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8\u05ea", + "connection_aborted": "\u05dc\u05d0 \u05e0\u05d9\u05ea\u05df \u05dc\u05d4\u05ea\u05d7\u05d1\u05e8 \u05dc\u05e9\u05e8\u05ea HMIP", "unknown": "\u05d0\u05d9\u05e8\u05e2\u05d4 \u05e9\u05d2\u05d9\u05d0\u05d4 \u05dc\u05d0 \u05d9\u05d3\u05d5\u05e2\u05d4." }, "error": { diff --git a/homeassistant/components/ios/.translations/he.json b/homeassistant/components/ios/.translations/he.json new file mode 100644 index 00000000000..e786e5ae843 --- /dev/null +++ b/homeassistant/components/ios/.translations/he.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u05e8\u05e7 \u05d4\u05d2\u05d3\u05e8\u05d4 \u05d0\u05d7\u05ea \u05e9\u05dc Home Assistant iOS \u05e0\u05d7\u05d5\u05e6\u05d4." + }, + "step": { + "confirm": { + "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05d0\u05ea Home Assistant iOS?", + "title": "Home Assistant iOS" + } + }, + "title": "Home Assistant iOS" + } +} \ No newline at end of file diff --git a/homeassistant/components/ios/.translations/sl.json b/homeassistant/components/ios/.translations/sl.json new file mode 100644 index 00000000000..28e9102aafd --- /dev/null +++ b/homeassistant/components/ios/.translations/sl.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Potrebna je samo ena konfiguracija Home Assistant iOS." + }, + "step": { + "confirm": { + "description": "Ali \u017eelite nastaviti komponento za Home Assistant iOS?", + "title": "Home Assistant iOS" + } + }, + "title": "Home Assistant iOS" + } +} \ No newline at end of file diff --git a/homeassistant/components/ios/.translations/sv.json b/homeassistant/components/ios/.translations/sv.json new file mode 100644 index 00000000000..6806f9bab90 --- /dev/null +++ b/homeassistant/components/ios/.translations/sv.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "confirm": { + "description": "Vill du konfigurera Home Assistants iOS komponent?", + "title": "Home Assistant iOS" + } + }, + "title": "Home Assistant iOS" + } +} \ No newline at end of file diff --git a/homeassistant/components/mqtt/.translations/ca.json b/homeassistant/components/mqtt/.translations/ca.json index 57e9a83d201..b6c73f35f26 100644 --- a/homeassistant/components/mqtt/.translations/ca.json +++ b/homeassistant/components/mqtt/.translations/ca.json @@ -10,6 +10,7 @@ "broker": { "data": { "broker": "Broker", + "discovery": "Activar descobreix automaticament", "password": "Contrasenya", "port": "Port", "username": "Nom d'usuari" diff --git a/homeassistant/components/mqtt/.translations/de.json b/homeassistant/components/mqtt/.translations/de.json index 15b6b3b9731..eeff1ca3041 100644 --- a/homeassistant/components/mqtt/.translations/de.json +++ b/homeassistant/components/mqtt/.translations/de.json @@ -1,5 +1,11 @@ { "config": { + "abort": { + "single_instance_allowed": "Nur eine einzige Konfiguration von MQTT ist zul\u00e4ssig." + }, + "error": { + "cannot_connect": "Es konnte keine Verbindung zum Broker hergestellt werden." + }, "step": { "broker": { "data": { diff --git a/homeassistant/components/mqtt/.translations/fr.json b/homeassistant/components/mqtt/.translations/fr.json index 1870c598e3b..916b4fdaf39 100644 --- a/homeassistant/components/mqtt/.translations/fr.json +++ b/homeassistant/components/mqtt/.translations/fr.json @@ -10,6 +10,7 @@ "broker": { "data": { "broker": "Broker", + "discovery": "Activer la d\u00e9couverte automatique", "password": "Mot de passe", "port": "Port", "username": "Nom d'utilisateur" diff --git a/homeassistant/components/mqtt/.translations/he.json b/homeassistant/components/mqtt/.translations/he.json new file mode 100644 index 00000000000..e1e2ed49748 --- /dev/null +++ b/homeassistant/components/mqtt/.translations/he.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u05e8\u05e7 \u05d4\u05d2\u05d3\u05e8\u05d4 \u05d0\u05d7\u05ea \u05e9\u05dc MQTT \u05de\u05d5\u05ea\u05e8\u05ea." + }, + "error": { + "cannot_connect": "\u05dc\u05d0 \u05e0\u05d9\u05ea\u05df \u05dc\u05d4\u05ea\u05d7\u05d1\u05e8 \u05dc\u05d1\u05e8\u05d5\u05e7\u05e8." + }, + "step": { + "broker": { + "data": { + "broker": "\u05d1\u05e8\u05d5\u05e7\u05e8", + "discovery": "\u05d0\u05e4\u05e9\u05e8 \u05d2\u05d9\u05dc\u05d5\u05d9", + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "port": "\u05e4\u05d5\u05e8\u05d8", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + }, + "description": "\u05e0\u05d0 \u05dc\u05d4\u05d6\u05d9\u05df \u05d0\u05ea \u05e4\u05e8\u05d8\u05d9 \u05d4\u05d7\u05d9\u05d1\u05d5\u05e8 \u05e9\u05dc \u05d4\u05d1\u05e8\u05d5\u05e7\u05e8 MQTT \u05e9\u05dc\u05da.", + "title": "MQTT" + } + }, + "title": "MQTT" + } +} \ No newline at end of file diff --git a/homeassistant/components/mqtt/.translations/lb.json b/homeassistant/components/mqtt/.translations/lb.json index 82a10194667..166fce9fbfb 100644 --- a/homeassistant/components/mqtt/.translations/lb.json +++ b/homeassistant/components/mqtt/.translations/lb.json @@ -10,6 +10,7 @@ "broker": { "data": { "broker": "Broker", + "discovery": "Entdeckung aktiv\u00e9ieren", "password": "Passwuert", "port": "Port", "username": "Benotzernumm" diff --git a/homeassistant/components/mqtt/.translations/pl.json b/homeassistant/components/mqtt/.translations/pl.json index f3ccaf8f37e..e87e550b98d 100644 --- a/homeassistant/components/mqtt/.translations/pl.json +++ b/homeassistant/components/mqtt/.translations/pl.json @@ -10,6 +10,7 @@ "broker": { "data": { "broker": "Po\u015brednik", + "discovery": "W\u0142\u0105cz wykrywanie", "password": "Has\u0142o", "port": "Port", "username": "Nazwa u\u017cytkownika" diff --git a/homeassistant/components/mqtt/.translations/ru.json b/homeassistant/components/mqtt/.translations/ru.json index c9dc3c2fd60..f1ff498dd72 100644 --- a/homeassistant/components/mqtt/.translations/ru.json +++ b/homeassistant/components/mqtt/.translations/ru.json @@ -10,6 +10,7 @@ "broker": { "data": { "broker": "\u0411\u0440\u043e\u043a\u0435\u0440", + "discovery": "\u0420\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u044c \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u0435", "password": "\u041f\u0430\u0440\u043e\u043b\u044c", "port": "\u041f\u043e\u0440\u0442", "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" diff --git a/homeassistant/components/mqtt/.translations/sl.json b/homeassistant/components/mqtt/.translations/sl.json new file mode 100644 index 00000000000..a12498ac4c2 --- /dev/null +++ b/homeassistant/components/mqtt/.translations/sl.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Dovoljena je samo ena konfiguracija MQTT." + }, + "error": { + "cannot_connect": "Ne morem se povezati na posrednik." + }, + "step": { + "broker": { + "data": { + "broker": "Posrednik", + "discovery": "Omogo\u010di odkrivanje", + "password": "Geslo", + "port": "port", + "username": "Uporabni\u0161ko ime" + }, + "description": "Prosimo vnesite informacije o povezavi va\u0161ega MQTT posrednika.", + "title": "MQTT" + } + }, + "title": "MQTT" + } +} \ No newline at end of file diff --git a/homeassistant/components/mqtt/.translations/sv.json b/homeassistant/components/mqtt/.translations/sv.json new file mode 100644 index 00000000000..7cf6d75b9c1 --- /dev/null +++ b/homeassistant/components/mqtt/.translations/sv.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "broker": { + "data": { + "password": "L\u00f6senord", + "port": "Port", + "username": "Anv\u00e4ndarnamn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mqtt/.translations/zh-Hans.json b/homeassistant/components/mqtt/.translations/zh-Hans.json index f539bd2a630..98a7d9eb4be 100644 --- a/homeassistant/components/mqtt/.translations/zh-Hans.json +++ b/homeassistant/components/mqtt/.translations/zh-Hans.json @@ -10,6 +10,7 @@ "broker": { "data": { "broker": "\u670d\u52a1\u5668", + "discovery": "\u542f\u7528\u53d1\u73b0", "password": "\u5bc6\u7801", "port": "\u7aef\u53e3", "username": "\u7528\u6237\u540d" diff --git a/homeassistant/components/mqtt/.translations/zh-Hant.json b/homeassistant/components/mqtt/.translations/zh-Hant.json index e6f27a439d5..cf87ceb8f98 100644 --- a/homeassistant/components/mqtt/.translations/zh-Hant.json +++ b/homeassistant/components/mqtt/.translations/zh-Hant.json @@ -10,6 +10,7 @@ "broker": { "data": { "broker": "Broker", + "discovery": "\u958b\u555f\u63a2\u7d22", "password": "\u4f7f\u7528\u8005\u5bc6\u78bc", "port": "\u901a\u8a0a\u57e0", "username": "\u4f7f\u7528\u8005\u540d\u7a31" diff --git a/homeassistant/components/nest/.translations/de.json b/homeassistant/components/nest/.translations/de.json index 86b50ab3c10..975d15e4470 100644 --- a/homeassistant/components/nest/.translations/de.json +++ b/homeassistant/components/nest/.translations/de.json @@ -24,7 +24,7 @@ "data": { "code": "PIN Code" }, - "description": "[Autorisieren Sie ihr Konto] ( {url} ), um ihren Nest-Account zu verkn\u00fcpfen.\n\n F\u00fcgen Sie anschlie\u00dfend den erhaltenen PIN Code hier ein.", + "description": "[Autorisiere dein Konto] ( {url} ), um deinen Nest-Account zu verkn\u00fcpfen.\n\n F\u00fcge anschlie\u00dfend den erhaltenen PIN Code hier ein.", "title": "Nest-Konto verkn\u00fcpfen" } }, diff --git a/homeassistant/components/openuv/.translations/de.json b/homeassistant/components/openuv/.translations/de.json index 1f81ac30f53..7f8121dd96b 100644 --- a/homeassistant/components/openuv/.translations/de.json +++ b/homeassistant/components/openuv/.translations/de.json @@ -11,7 +11,8 @@ "elevation": "H\u00f6he", "latitude": "Breitengrad", "longitude": "L\u00e4ngengrad" - } + }, + "title": "Gebe deine Informationen ein" } }, "title": "OpenUV" diff --git a/homeassistant/components/openuv/.translations/he.json b/homeassistant/components/openuv/.translations/he.json new file mode 100644 index 00000000000..262a3d732a2 --- /dev/null +++ b/homeassistant/components/openuv/.translations/he.json @@ -0,0 +1,20 @@ +{ + "config": { + "error": { + "identifier_exists": "\u05d4\u05e7\u05d5\u05d0\u05d5\u05e8\u05d3\u05d9\u05e0\u05d8\u05d5\u05ea \u05db\u05d1\u05e8 \u05e8\u05e9\u05d5\u05de\u05d5\u05ea", + "invalid_api_key": "\u05de\u05e4\u05ea\u05d7 API \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" + }, + "step": { + "user": { + "data": { + "api_key": "\u05de\u05e4\u05ea\u05d7 API \u05e9\u05dc OpenUV", + "elevation": "\u05d2\u05d5\u05d1\u05d4 \u05de\u05e2\u05dc \u05e4\u05e0\u05d9 \u05d4\u05d9\u05dd", + "latitude": "\u05e7\u05d5 \u05e8\u05d5\u05d7\u05d1", + "longitude": "\u05e7\u05d5 \u05d0\u05d5\u05e8\u05da" + }, + "title": "\u05de\u05dc\u05d0 \u05d0\u05ea \u05d4\u05e4\u05e8\u05d8\u05d9\u05dd \u05e9\u05dc\u05da" + } + }, + "title": "OpenUV" + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/moon.he.json b/homeassistant/components/sensor/.translations/moon.he.json index 60999f83645..6531d3c8265 100644 --- a/homeassistant/components/sensor/.translations/moon.he.json +++ b/homeassistant/components/sensor/.translations/moon.he.json @@ -3,6 +3,10 @@ "first_quarter": "\u05e8\u05d1\u05e2\u05d5\u05df \u05e8\u05d0\u05e9\u05d5\u05df", "full_moon": "\u05d9\u05e8\u05d7 \u05de\u05dc\u05d0", "last_quarter": "\u05e8\u05d1\u05e2\u05d5\u05df \u05d0\u05d7\u05e8\u05d5\u05df", - "new_moon": "\u05e8\u05d0\u05e9 \u05d7\u05d5\u05d3\u05e9" + "new_moon": "\u05e8\u05d0\u05e9 \u05d7\u05d5\u05d3\u05e9", + "waning_crescent": "Waning crescent", + "waning_gibbous": "Waning gibbous", + "waxing_crescent": "Waxing crescent", + "waxing_gibbous": "Waxing gibbous" } } \ No newline at end of file diff --git a/homeassistant/components/sonos/.translations/de.json b/homeassistant/components/sonos/.translations/de.json index dd44fca5888..920d25a3bfa 100644 --- a/homeassistant/components/sonos/.translations/de.json +++ b/homeassistant/components/sonos/.translations/de.json @@ -6,7 +6,7 @@ }, "step": { "confirm": { - "description": "M\u00f6chten Sie Sonos einrichten?", + "description": "M\u00f6chtest du Sonos einrichten?", "title": "Sonos" } }, diff --git a/homeassistant/components/tradfri/.translations/de.json b/homeassistant/components/tradfri/.translations/de.json new file mode 100644 index 00000000000..5284ae18b6d --- /dev/null +++ b/homeassistant/components/tradfri/.translations/de.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Bridge ist bereits konfiguriert" + }, + "error": { + "cannot_connect": "Verbindung zum Gateway nicht m\u00f6glich.", + "invalid_key": "Fehler beim Registrieren mit dem angegebenen Schl\u00fcssel. Wenn dies weiterhin geschieht, versuche, das Gateway neu zu starten.", + "timeout": "Timeout bei der \u00dcberpr\u00fcfung des Codes." + }, + "step": { + "auth": { + "data": { + "security_code": "Sicherheitscode" + }, + "description": "Du findest den Sicherheitscode auf der R\u00fcckseite deines Gateways.", + "title": "Sicherheitscode eingeben" + } + }, + "title": "IKEA TR\u00c5DFRI" + } +} \ No newline at end of file diff --git a/homeassistant/components/tradfri/.translations/he.json b/homeassistant/components/tradfri/.translations/he.json new file mode 100644 index 00000000000..09af3d09bdc --- /dev/null +++ b/homeassistant/components/tradfri/.translations/he.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "\u05d4\u05de\u05d2\u05e9\u05e8 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8" + }, + "error": { + "cannot_connect": "\u05dc\u05d0 \u05e0\u05d9\u05ea\u05df \u05dc\u05d4\u05ea\u05d7\u05d1\u05e8 \u05dc\u05de\u05d2\u05e9\u05e8", + "invalid_key": "\u05d4\u05e8\u05d9\u05e9\u05d5\u05dd \u05e0\u05db\u05e9\u05dc \u05e2\u05dd \u05d4\u05de\u05e4\u05ea\u05d7 \u05e9\u05e1\u05d5\u05e4\u05e7. \u05d0\u05dd \u05d6\u05d4 \u05e7\u05d5\u05e8\u05d4 \u05e9\u05d5\u05d1, \u05e0\u05e1\u05d4 \u05dc\u05d4\u05e4\u05e2\u05d9\u05dc \u05de\u05d7\u05d3\u05e9 \u05d0\u05ea \u05d4\u05de\u05d2\u05e9\u05e8.", + "timeout": "\u05e2\u05d1\u05e8 \u05d4\u05d6\u05de\u05df \u05d4\u05e7\u05e6\u05d5\u05d1 \u05dc\u05d0\u05d9\u05de\u05d5\u05ea \u05d4\u05e7\u05d5\u05d3" + }, + "step": { + "auth": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7", + "security_code": "\u05e7\u05d5\u05d3 \u05d0\u05d1\u05d8\u05d7\u05d4" + }, + "description": "\u05ea\u05d5\u05db\u05dc \u05dc\u05de\u05e6\u05d5\u05d0 \u05d0\u05ea \u05e7\u05d5\u05d3 \u05d4\u05d0\u05d1\u05d8\u05d7\u05d4 \u05d1\u05d2\u05d1 \u05d4\u05de\u05d2\u05e9\u05e8 \u05e9\u05dc\u05da.", + "title": "\u05d4\u05d6\u05df \u05e7\u05d5\u05d3 \u05d0\u05d1\u05d8\u05d7\u05d4" + } + }, + "title": "IKEA TR\u00c5DFRI" + } +} \ No newline at end of file diff --git a/homeassistant/components/tradfri/.translations/pl.json b/homeassistant/components/tradfri/.translations/pl.json new file mode 100644 index 00000000000..ec253447ef4 --- /dev/null +++ b/homeassistant/components/tradfri/.translations/pl.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Mostek jest ju\u017c skonfigurowany" + }, + "error": { + "cannot_connect": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z bram\u0105.", + "invalid_key": "Rejestracja si\u0119 nie powiod\u0142a z podanym kluczem. Je\u015bli tak si\u0119 stanie, spr\u00f3buj ponownie uruchomi\u0107 bramk\u0119.", + "timeout": "Min\u0105\u0142 limit czasu sprawdzania poprawno\u015bci kodu" + }, + "step": { + "auth": { + "data": { + "host": "Host", + "security_code": "Kod bezpiecze\u0144stwa" + }, + "description": "Mo\u017cesz znale\u017a\u0107 kod bezpiecze\u0144stwa z ty\u0142u bramy.", + "title": "Wprowad\u017a kod bezpiecze\u0144stwa" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tradfri/.translations/sl.json b/homeassistant/components/tradfri/.translations/sl.json new file mode 100644 index 00000000000..ee2bf7d3d2b --- /dev/null +++ b/homeassistant/components/tradfri/.translations/sl.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Most je \u017ee konfiguriran" + }, + "error": { + "cannot_connect": "Povezava s prehodom ni mogo\u010de.", + "invalid_key": "Ni se bilo mogo\u010de registrirati s prilo\u017eenim klju\u010dem. \u010ce se to dogaja, poskusite znova zagnati prehod.", + "timeout": "\u010casovna omejitev za potrditev kode je potekla." + }, + "step": { + "auth": { + "data": { + "host": "Gostitelj", + "security_code": "Varnostna koda" + }, + "description": "Varnostno kodo najdete na hrbtni strani va\u0161ega prehoda.", + "title": "Vnesite varnostno kodo" + } + }, + "title": "IKEA TR\u00c5DFRI" + } +} \ No newline at end of file diff --git a/homeassistant/components/tradfri/.translations/sv.json b/homeassistant/components/tradfri/.translations/sv.json new file mode 100644 index 00000000000..ffe8bff22b4 --- /dev/null +++ b/homeassistant/components/tradfri/.translations/sv.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Bryggan \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta till gatewayen." + }, + "step": { + "auth": { + "data": { + "security_code": "S\u00e4kerhetskod" + }, + "description": "Du kan hitta s\u00e4kerhetskoden p\u00e5 baksidan av din gateway.", + "title": "Ange s\u00e4kerhetskod" + } + }, + "title": "IKEA TR\u00c5DFRI" + } +} \ No newline at end of file diff --git a/homeassistant/components/tradfri/.translations/zh-Hans.json b/homeassistant/components/tradfri/.translations/zh-Hans.json index 28a9c676f2e..4791e46062a 100644 --- a/homeassistant/components/tradfri/.translations/zh-Hans.json +++ b/homeassistant/components/tradfri/.translations/zh-Hans.json @@ -7,6 +7,17 @@ "cannot_connect": "\u65e0\u6cd5\u8fde\u63a5\u5230\u7f51\u5173\u3002", "invalid_key": "\u65e0\u6cd5\u7528\u63d0\u4f9b\u7684\u5bc6\u94a5\u6ce8\u518c\u3002\u5982\u679c\u9519\u8bef\u6301\u7eed\u53d1\u751f\uff0c\u8bf7\u5c1d\u8bd5\u91cd\u65b0\u542f\u52a8\u7f51\u5173\u3002", "timeout": "\u4ee3\u7801\u9a8c\u8bc1\u8d85\u65f6" - } + }, + "step": { + "auth": { + "data": { + "host": "\u4e3b\u673a", + "security_code": "\u5b89\u5168\u7801" + }, + "description": "\u60a8\u53ef\u4ee5\u5728\u7f51\u5173\u80cc\u9762\u627e\u5230\u5b89\u5168\u7801\u3002", + "title": "\u8f93\u5165\u5b89\u5168\u7801" + } + }, + "title": "IKEA TR\u00c5DFRI" } } \ No newline at end of file From 9ab8f78b192bc47eb099b9d560029d422dfb6ce5 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 26 Sep 2018 17:20:34 +0200 Subject: [PATCH 073/247] Don't pass use_env=True (#16896) --- homeassistant/helpers/aiohttp_client.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/helpers/aiohttp_client.py b/homeassistant/helpers/aiohttp_client.py index 01cc074aa6a..53b246c700d 100644 --- a/homeassistant/helpers/aiohttp_client.py +++ b/homeassistant/helpers/aiohttp_client.py @@ -56,7 +56,6 @@ def async_create_clientsession(hass, verify_ssl=True, auto_cleanup=True, clientsession = aiohttp.ClientSession( loop=hass.loop, connector=connector, - trust_env=True, headers={USER_AGENT: SERVER_SOFTWARE}, **kwargs ) From d0ddc28f96276edd80c9da425d57acbd0c98f276 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 26 Sep 2018 17:58:44 +0200 Subject: [PATCH 074/247] Revert file mode write_json (#16897) --- homeassistant/util/json.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/homeassistant/util/json.py b/homeassistant/util/json.py index 40f25689148..d6f7e149040 100644 --- a/homeassistant/util/json.py +++ b/homeassistant/util/json.py @@ -47,9 +47,7 @@ def save_json(filename: str, data: Union[List, Dict], """ try: json_data = json.dumps(data, sort_keys=True, indent=4) - mode = 0o600 if private else 0o644 - with open(os.open(filename, os.O_WRONLY | os.O_CREAT, mode), - 'w', encoding='utf-8') as fdesc: + with open(filename, 'w', encoding='utf-8') as fdesc: fdesc.write(json_data) except TypeError as error: _LOGGER.exception('Failed to serialize to JSON: %s', From dd45e99302d93973f04514d02be3ee5d14bb9ea1 Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Wed, 26 Sep 2018 18:02:05 +0200 Subject: [PATCH 075/247] Remove service helper (4) (#16892) * Update media_player * Update lock * Update notify * Update remote * Update scene * Update vacuum * Remove timer helpers * Removed unused legacy helpers --- homeassistant/components/lock/__init__.py | 36 ---- .../components/media_player/__init__.py | 162 ------------------ homeassistant/components/notify/__init__.py | 17 -- homeassistant/components/remote/__init__.py | 57 ------ homeassistant/components/scene/__init__.py | 12 -- homeassistant/components/timer/__init__.py | 60 ------- homeassistant/components/vacuum/__init__.py | 88 ---------- tests/components/lock/common.py | 45 +++++ tests/components/lock/test_demo.py | 8 +- tests/components/lock/test_mqtt.py | 6 +- tests/components/media_player/common.py | 150 ++++++++++++++++ tests/components/media_player/test_demo.py | 51 +++--- tests/components/notify/common.py | 24 +++ tests/components/notify/test_demo.py | 8 +- tests/components/remote/common.py | 55 ++++++ tests/components/remote/test_demo.py | 10 +- tests/components/remote/test_init.py | 8 +- tests/components/scene/common.py | 19 ++ tests/components/scene/test_init.py | 7 +- tests/components/scene/test_litejet.py | 5 +- tests/components/vacuum/common.py | 101 +++++++++++ tests/components/vacuum/test_demo.py | 70 ++++---- tests/components/vacuum/test_mqtt.py | 20 ++- 23 files changed, 499 insertions(+), 520 deletions(-) create mode 100644 tests/components/lock/common.py create mode 100644 tests/components/media_player/common.py create mode 100644 tests/components/notify/common.py create mode 100644 tests/components/remote/common.py create mode 100644 tests/components/scene/common.py create mode 100644 tests/components/vacuum/common.py diff --git a/homeassistant/components/lock/__init__.py b/homeassistant/components/lock/__init__.py index 3c4ff7cdedd..5218ea49c80 100644 --- a/homeassistant/components/lock/__init__.py +++ b/homeassistant/components/lock/__init__.py @@ -57,42 +57,6 @@ def is_locked(hass, entity_id=None): return hass.states.is_state(entity_id, STATE_LOCKED) -@bind_hass -def lock(hass, entity_id=None, code=None): - """Lock all or specified locks.""" - data = {} - if code: - data[ATTR_CODE] = code - if entity_id: - data[ATTR_ENTITY_ID] = entity_id - - hass.services.call(DOMAIN, SERVICE_LOCK, data) - - -@bind_hass -def unlock(hass, entity_id=None, code=None): - """Unlock all or specified locks.""" - data = {} - if code: - data[ATTR_CODE] = code - if entity_id: - data[ATTR_ENTITY_ID] = entity_id - - hass.services.call(DOMAIN, SERVICE_UNLOCK, data) - - -@bind_hass -def open_lock(hass, entity_id=None, code=None): - """Open all or specified locks.""" - data = {} - if code: - data[ATTR_CODE] = code - if entity_id: - data[ATTR_ENTITY_ID] = entity_id - - hass.services.call(DOMAIN, SERVICE_OPEN, data) - - @asyncio.coroutine def async_setup(hass, config): """Track states and offer events for locks.""" diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 831009ed8bf..235ca8d5b2d 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -192,168 +192,6 @@ def is_on(hass, entity_id=None): for entity_id in entity_ids) -@bind_hass -def turn_on(hass, entity_id=None): - """Turn on specified media player or all.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} - hass.services.call(DOMAIN, SERVICE_TURN_ON, data) - - -@bind_hass -def turn_off(hass, entity_id=None): - """Turn off specified media player or all.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} - hass.services.call(DOMAIN, SERVICE_TURN_OFF, data) - - -@bind_hass -def toggle(hass, entity_id=None): - """Toggle specified media player or all.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} - hass.services.call(DOMAIN, SERVICE_TOGGLE, data) - - -@bind_hass -def volume_up(hass, entity_id=None): - """Send the media player the command for volume up.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} - hass.services.call(DOMAIN, SERVICE_VOLUME_UP, data) - - -@bind_hass -def volume_down(hass, entity_id=None): - """Send the media player the command for volume down.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} - hass.services.call(DOMAIN, SERVICE_VOLUME_DOWN, data) - - -@bind_hass -def mute_volume(hass, mute, entity_id=None): - """Send the media player the command for muting the volume.""" - data = {ATTR_MEDIA_VOLUME_MUTED: mute} - - if entity_id: - data[ATTR_ENTITY_ID] = entity_id - - hass.services.call(DOMAIN, SERVICE_VOLUME_MUTE, data) - - -@bind_hass -def set_volume_level(hass, volume, entity_id=None): - """Send the media player the command for setting the volume.""" - data = {ATTR_MEDIA_VOLUME_LEVEL: volume} - - if entity_id: - data[ATTR_ENTITY_ID] = entity_id - - hass.services.call(DOMAIN, SERVICE_VOLUME_SET, data) - - -@bind_hass -def media_play_pause(hass, entity_id=None): - """Send the media player the command for play/pause.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} - hass.services.call(DOMAIN, SERVICE_MEDIA_PLAY_PAUSE, data) - - -@bind_hass -def media_play(hass, entity_id=None): - """Send the media player the command for play/pause.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} - hass.services.call(DOMAIN, SERVICE_MEDIA_PLAY, data) - - -@bind_hass -def media_pause(hass, entity_id=None): - """Send the media player the command for pause.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} - hass.services.call(DOMAIN, SERVICE_MEDIA_PAUSE, data) - - -@bind_hass -def media_stop(hass, entity_id=None): - """Send the media player the stop command.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} - hass.services.call(DOMAIN, SERVICE_MEDIA_STOP, data) - - -@bind_hass -def media_next_track(hass, entity_id=None): - """Send the media player the command for next track.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} - hass.services.call(DOMAIN, SERVICE_MEDIA_NEXT_TRACK, data) - - -@bind_hass -def media_previous_track(hass, entity_id=None): - """Send the media player the command for prev track.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} - hass.services.call(DOMAIN, SERVICE_MEDIA_PREVIOUS_TRACK, data) - - -@bind_hass -def media_seek(hass, position, entity_id=None): - """Send the media player the command to seek in current playing media.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} - data[ATTR_MEDIA_SEEK_POSITION] = position - hass.services.call(DOMAIN, SERVICE_MEDIA_SEEK, data) - - -@bind_hass -def play_media(hass, media_type, media_id, entity_id=None, enqueue=None): - """Send the media player the command for playing media.""" - data = {ATTR_MEDIA_CONTENT_TYPE: media_type, - ATTR_MEDIA_CONTENT_ID: media_id} - - if entity_id: - data[ATTR_ENTITY_ID] = entity_id - - if enqueue: - data[ATTR_MEDIA_ENQUEUE] = enqueue - - hass.services.call(DOMAIN, SERVICE_PLAY_MEDIA, data) - - -@bind_hass -def select_source(hass, source, entity_id=None): - """Send the media player the command to select input source.""" - data = {ATTR_INPUT_SOURCE: source} - - if entity_id: - data[ATTR_ENTITY_ID] = entity_id - - hass.services.call(DOMAIN, SERVICE_SELECT_SOURCE, data) - - -@bind_hass -def select_sound_mode(hass, sound_mode, entity_id=None): - """Send the media player the command to select sound mode.""" - data = {ATTR_SOUND_MODE: sound_mode} - - if entity_id: - data[ATTR_ENTITY_ID] = entity_id - - hass.services.call(DOMAIN, SERVICE_SELECT_SOUND_MODE, data) - - -@bind_hass -def clear_playlist(hass, entity_id=None): - """Send the media player the command for clear playlist.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} - hass.services.call(DOMAIN, SERVICE_CLEAR_PLAYLIST, data) - - -@bind_hass -def set_shuffle(hass, shuffle, entity_id=None): - """Send the media player the command to enable/disable shuffle mode.""" - data = {ATTR_MEDIA_SHUFFLE: shuffle} - - if entity_id: - data[ATTR_ENTITY_ID] = entity_id - - hass.services.call(DOMAIN, SERVICE_SHUFFLE_SET, data) - - WS_TYPE_MEDIA_PLAYER_THUMBNAIL = 'media_player_thumbnail' SCHEMA_WEBSOCKET_GET_THUMBNAIL = \ websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ diff --git a/homeassistant/components/notify/__init__.py b/homeassistant/components/notify/__init__.py index 4de35d3f850..0535d7caa6d 100644 --- a/homeassistant/components/notify/__init__.py +++ b/homeassistant/components/notify/__init__.py @@ -12,7 +12,6 @@ import voluptuous as vol from homeassistant.setup import async_prepare_setup_platform from homeassistant.exceptions import HomeAssistantError -from homeassistant.loader import bind_hass import homeassistant.helpers.config_validation as cv from homeassistant.const import CONF_NAME, CONF_PLATFORM from homeassistant.helpers import config_per_platform, discovery @@ -50,22 +49,6 @@ NOTIFY_SERVICE_SCHEMA = vol.Schema({ }) -@bind_hass -def send_message(hass, message, title=None, data=None): - """Send a notification message.""" - info = { - ATTR_MESSAGE: message - } - - if title is not None: - info[ATTR_TITLE] = title - - if data is not None: - info[ATTR_DATA] = data - - hass.services.call(DOMAIN, SERVICE_NOTIFY, info) - - @asyncio.coroutine def async_setup(hass, config): """Set up the notify services.""" diff --git a/homeassistant/components/remote/__init__.py b/homeassistant/components/remote/__init__.py index 11ecb20f7aa..3fd2a5d4c44 100644 --- a/homeassistant/components/remote/__init__.py +++ b/homeassistant/components/remote/__init__.py @@ -70,63 +70,6 @@ def is_on(hass, entity_id=None): return hass.states.is_state(entity_id, STATE_ON) -@bind_hass -def turn_on(hass, activity=None, entity_id=None): - """Turn all or specified remote on.""" - data = { - key: value for key, value in [ - (ATTR_ACTIVITY, activity), - (ATTR_ENTITY_ID, entity_id), - ] if value is not None} - hass.services.call(DOMAIN, SERVICE_TURN_ON, data) - - -@bind_hass -def turn_off(hass, activity=None, entity_id=None): - """Turn all or specified remote off.""" - data = {} - if activity: - data[ATTR_ACTIVITY] = activity - - if entity_id: - data[ATTR_ENTITY_ID] = entity_id - - hass.services.call(DOMAIN, SERVICE_TURN_OFF, data) - - -@bind_hass -def toggle(hass, activity=None, entity_id=None): - """Toggle all or specified remote.""" - data = {} - if activity: - data[ATTR_ACTIVITY] = activity - - if entity_id: - data[ATTR_ENTITY_ID] = entity_id - - hass.services.call(DOMAIN, SERVICE_TOGGLE, data) - - -@bind_hass -def send_command(hass, command, entity_id=None, device=None, - num_repeats=None, delay_secs=None): - """Send a command to a device.""" - data = {ATTR_COMMAND: command} - if entity_id: - data[ATTR_ENTITY_ID] = entity_id - - if device: - data[ATTR_DEVICE] = device - - if num_repeats: - data[ATTR_NUM_REPEATS] = num_repeats - - if delay_secs: - data[ATTR_DELAY_SECS] = delay_secs - - hass.services.call(DOMAIN, SERVICE_SEND_COMMAND, data) - - @asyncio.coroutine def async_setup(hass, config): """Track states and offer events for remotes.""" diff --git a/homeassistant/components/scene/__init__.py b/homeassistant/components/scene/__init__.py index 8771a84c1d6..2bcb1c8e16d 100644 --- a/homeassistant/components/scene/__init__.py +++ b/homeassistant/components/scene/__init__.py @@ -12,7 +12,6 @@ import voluptuous as vol from homeassistant.const import ( ATTR_ENTITY_ID, CONF_PLATFORM, SERVICE_TURN_ON) -from homeassistant.loader import bind_hass import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent @@ -61,17 +60,6 @@ SCENE_SERVICE_SCHEMA = vol.Schema({ }) -@bind_hass -def activate(hass, entity_id=None): - """Activate a scene.""" - data = {} - - if entity_id: - data[ATTR_ENTITY_ID] = entity_id - - hass.services.call(DOMAIN, SERVICE_TURN_ON, data) - - async def async_setup(hass, config): """Set up the scenes.""" logger = logging.getLogger(__name__) diff --git a/homeassistant/components/timer/__init__.py b/homeassistant/components/timer/__init__.py index 8406b3ff5ec..c29df9db858 100644 --- a/homeassistant/components/timer/__init__.py +++ b/homeassistant/components/timer/__init__.py @@ -12,12 +12,10 @@ import voluptuous as vol import homeassistant.util.dt as dt_util import homeassistant.helpers.config_validation as cv from homeassistant.const import (ATTR_ENTITY_ID, CONF_ICON, CONF_NAME) -from homeassistant.core import callback from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.event import async_track_point_in_utc_time -from homeassistant.loader import bind_hass _LOGGER = logging.getLogger(__name__) @@ -63,64 +61,6 @@ CONFIG_SCHEMA = vol.Schema({ }, extra=vol.ALLOW_EXTRA) -@bind_hass -def start(hass, entity_id, duration): - """Start a timer.""" - hass.add_job(async_start, hass, entity_id, {ATTR_ENTITY_ID: entity_id, - ATTR_DURATION: duration}) - - -@callback -@bind_hass -def async_start(hass, entity_id, duration): - """Start a timer.""" - hass.async_add_job(hass.services.async_call( - DOMAIN, SERVICE_START, {ATTR_ENTITY_ID: entity_id, - ATTR_DURATION: duration})) - - -@bind_hass -def pause(hass, entity_id): - """Pause a timer.""" - hass.add_job(async_pause, hass, entity_id) - - -@callback -@bind_hass -def async_pause(hass, entity_id): - """Pause a timer.""" - hass.async_add_job(hass.services.async_call( - DOMAIN, SERVICE_PAUSE, {ATTR_ENTITY_ID: entity_id})) - - -@bind_hass -def cancel(hass, entity_id): - """Cancel a timer.""" - hass.add_job(async_cancel, hass, entity_id) - - -@callback -@bind_hass -def async_cancel(hass, entity_id): - """Cancel a timer.""" - hass.async_add_job(hass.services.async_call( - DOMAIN, SERVICE_CANCEL, {ATTR_ENTITY_ID: entity_id})) - - -@bind_hass -def finish(hass, entity_id): - """Finish a timer.""" - hass.add_job(async_cancel, hass, entity_id) - - -@callback -@bind_hass -def async_finish(hass, entity_id): - """Finish a timer.""" - hass.async_add_job(hass.services.async_call( - DOMAIN, SERVICE_FINISH, {ATTR_ENTITY_ID: entity_id})) - - async def async_setup(hass, config): """Set up a timer.""" component = EntityComponent(_LOGGER, DOMAIN, hass) diff --git a/homeassistant/components/vacuum/__init__.py b/homeassistant/components/vacuum/__init__.py index 1808737d281..7a1c3049152 100644 --- a/homeassistant/components/vacuum/__init__.py +++ b/homeassistant/components/vacuum/__init__.py @@ -94,94 +94,6 @@ def is_on(hass, entity_id=None): return hass.states.is_state(entity_id, STATE_ON) -@bind_hass -def 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) - - -@bind_hass -def 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) - - -@bind_hass -def 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) - - -@bind_hass -def 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) - - -@bind_hass -def 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) - - -@bind_hass -def 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) - - -@bind_hass -def 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) - - -@bind_hass -def 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) - - -@bind_hass -def 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) - - -@bind_hass -def 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) - - -@bind_hass -def 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) - - -@bind_hass -def 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) - - @asyncio.coroutine def async_setup(hass, config): """Set up the vacuum component.""" diff --git a/tests/components/lock/common.py b/tests/components/lock/common.py new file mode 100644 index 00000000000..2150b3cb894 --- /dev/null +++ b/tests/components/lock/common.py @@ -0,0 +1,45 @@ +"""Collection of helper methods. + +All containing methods are legacy helpers that should not be used by new +components. Instead call the service directly. +""" +from homeassistant.components.lock import DOMAIN +from homeassistant.const import ( + ATTR_CODE, ATTR_ENTITY_ID, SERVICE_LOCK, SERVICE_UNLOCK, SERVICE_OPEN) +from homeassistant.loader import bind_hass + + +@bind_hass +def lock(hass, entity_id=None, code=None): + """Lock all or specified locks.""" + data = {} + if code: + data[ATTR_CODE] = code + if entity_id: + data[ATTR_ENTITY_ID] = entity_id + + hass.services.call(DOMAIN, SERVICE_LOCK, data) + + +@bind_hass +def unlock(hass, entity_id=None, code=None): + """Unlock all or specified locks.""" + data = {} + if code: + data[ATTR_CODE] = code + if entity_id: + data[ATTR_ENTITY_ID] = entity_id + + hass.services.call(DOMAIN, SERVICE_UNLOCK, data) + + +@bind_hass +def open_lock(hass, entity_id=None, code=None): + """Open all or specified locks.""" + data = {} + if code: + data[ATTR_CODE] = code + if entity_id: + data[ATTR_ENTITY_ID] = entity_id + + hass.services.call(DOMAIN, SERVICE_OPEN, data) diff --git a/tests/components/lock/test_demo.py b/tests/components/lock/test_demo.py index 500cc7f9a6a..255e5307f7a 100644 --- a/tests/components/lock/test_demo.py +++ b/tests/components/lock/test_demo.py @@ -5,6 +5,8 @@ from homeassistant.setup import setup_component from homeassistant.components import lock from tests.common import get_test_home_assistant, mock_service +from tests.components.lock import common + FRONT = 'lock.front_door' KITCHEN = 'lock.kitchen_door' OPENABLE_LOCK = 'lock.openable_lock' @@ -36,14 +38,14 @@ class TestLockDemo(unittest.TestCase): def test_locking(self): """Test the locking of a lock.""" - lock.lock(self.hass, KITCHEN) + common.lock(self.hass, KITCHEN) self.hass.block_till_done() self.assertTrue(lock.is_locked(self.hass, KITCHEN)) def test_unlocking(self): """Test the unlocking of a lock.""" - lock.unlock(self.hass, FRONT) + common.unlock(self.hass, FRONT) self.hass.block_till_done() self.assertFalse(lock.is_locked(self.hass, FRONT)) @@ -51,6 +53,6 @@ class TestLockDemo(unittest.TestCase): def test_opening(self): """Test the opening of a lock.""" calls = mock_service(self.hass, lock.DOMAIN, lock.SERVICE_OPEN) - lock.open_lock(self.hass, OPENABLE_LOCK) + common.open_lock(self.hass, OPENABLE_LOCK) self.hass.block_till_done() self.assertEqual(1, len(calls)) diff --git a/tests/components/lock/test_mqtt.py b/tests/components/lock/test_mqtt.py index f85c6a11774..4d2378ff9fa 100644 --- a/tests/components/lock/test_mqtt.py +++ b/tests/components/lock/test_mqtt.py @@ -6,9 +6,11 @@ from homeassistant.const import ( STATE_LOCKED, STATE_UNLOCKED, STATE_UNAVAILABLE, ATTR_ASSUMED_STATE) import homeassistant.components.lock as lock from homeassistant.components.mqtt.discovery import async_start + from tests.common import ( mock_mqtt_component, async_fire_mqtt_message, fire_mqtt_message, get_test_home_assistant) +from tests.components.lock import common class TestLockMQTT(unittest.TestCase): @@ -69,7 +71,7 @@ class TestLockMQTT(unittest.TestCase): self.assertEqual(STATE_UNLOCKED, state.state) self.assertTrue(state.attributes.get(ATTR_ASSUMED_STATE)) - lock.lock(self.hass, 'lock.test') + common.lock(self.hass, 'lock.test') self.hass.block_till_done() self.mock_publish.async_publish.assert_called_once_with( @@ -78,7 +80,7 @@ class TestLockMQTT(unittest.TestCase): state = self.hass.states.get('lock.test') self.assertEqual(STATE_LOCKED, state.state) - lock.unlock(self.hass, 'lock.test') + common.unlock(self.hass, 'lock.test') self.hass.block_till_done() self.mock_publish.async_publish.assert_called_once_with( diff --git a/tests/components/media_player/common.py b/tests/components/media_player/common.py new file mode 100644 index 00000000000..3f4d4cb9f24 --- /dev/null +++ b/tests/components/media_player/common.py @@ -0,0 +1,150 @@ +"""Collection of helper methods. + +All containing methods are legacy helpers that should not be used by new +components. Instead call the service directly. +""" +from homeassistant.components.media_player import ( + ATTR_INPUT_SOURCE, ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_CONTENT_TYPE, + ATTR_MEDIA_ENQUEUE, ATTR_MEDIA_SEEK_POSITION, ATTR_MEDIA_VOLUME_LEVEL, + ATTR_MEDIA_VOLUME_MUTED, DOMAIN, SERVICE_CLEAR_PLAYLIST, + SERVICE_PLAY_MEDIA, SERVICE_SELECT_SOURCE) +from homeassistant.const import ( + ATTR_ENTITY_ID, SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PAUSE, + SERVICE_MEDIA_PLAY, SERVICE_MEDIA_PLAY_PAUSE, SERVICE_MEDIA_PREVIOUS_TRACK, + SERVICE_MEDIA_SEEK, SERVICE_TOGGLE, SERVICE_TURN_OFF, SERVICE_TURN_ON, + SERVICE_VOLUME_DOWN, SERVICE_VOLUME_MUTE, SERVICE_VOLUME_SET, + SERVICE_VOLUME_UP) +from homeassistant.loader import bind_hass + + +@bind_hass +def turn_on(hass, entity_id=None): + """Turn on specified media player or all.""" + data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} + hass.services.call(DOMAIN, SERVICE_TURN_ON, data) + + +@bind_hass +def turn_off(hass, entity_id=None): + """Turn off specified media player or all.""" + data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} + hass.services.call(DOMAIN, SERVICE_TURN_OFF, data) + + +@bind_hass +def toggle(hass, entity_id=None): + """Toggle specified media player or all.""" + data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} + hass.services.call(DOMAIN, SERVICE_TOGGLE, data) + + +@bind_hass +def volume_up(hass, entity_id=None): + """Send the media player the command for volume up.""" + data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} + hass.services.call(DOMAIN, SERVICE_VOLUME_UP, data) + + +@bind_hass +def volume_down(hass, entity_id=None): + """Send the media player the command for volume down.""" + data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} + hass.services.call(DOMAIN, SERVICE_VOLUME_DOWN, data) + + +@bind_hass +def mute_volume(hass, mute, entity_id=None): + """Send the media player the command for muting the volume.""" + data = {ATTR_MEDIA_VOLUME_MUTED: mute} + + if entity_id: + data[ATTR_ENTITY_ID] = entity_id + + hass.services.call(DOMAIN, SERVICE_VOLUME_MUTE, data) + + +@bind_hass +def set_volume_level(hass, volume, entity_id=None): + """Send the media player the command for setting the volume.""" + data = {ATTR_MEDIA_VOLUME_LEVEL: volume} + + if entity_id: + data[ATTR_ENTITY_ID] = entity_id + + hass.services.call(DOMAIN, SERVICE_VOLUME_SET, data) + + +@bind_hass +def media_play_pause(hass, entity_id=None): + """Send the media player the command for play/pause.""" + data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} + hass.services.call(DOMAIN, SERVICE_MEDIA_PLAY_PAUSE, data) + + +@bind_hass +def media_play(hass, entity_id=None): + """Send the media player the command for play/pause.""" + data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} + hass.services.call(DOMAIN, SERVICE_MEDIA_PLAY, data) + + +@bind_hass +def media_pause(hass, entity_id=None): + """Send the media player the command for pause.""" + data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} + hass.services.call(DOMAIN, SERVICE_MEDIA_PAUSE, data) + + +@bind_hass +def media_next_track(hass, entity_id=None): + """Send the media player the command for next track.""" + data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} + hass.services.call(DOMAIN, SERVICE_MEDIA_NEXT_TRACK, data) + + +@bind_hass +def media_previous_track(hass, entity_id=None): + """Send the media player the command for prev track.""" + data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} + hass.services.call(DOMAIN, SERVICE_MEDIA_PREVIOUS_TRACK, data) + + +@bind_hass +def media_seek(hass, position, entity_id=None): + """Send the media player the command to seek in current playing media.""" + data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} + data[ATTR_MEDIA_SEEK_POSITION] = position + hass.services.call(DOMAIN, SERVICE_MEDIA_SEEK, data) + + +@bind_hass +def play_media(hass, media_type, media_id, entity_id=None, enqueue=None): + """Send the media player the command for playing media.""" + data = {ATTR_MEDIA_CONTENT_TYPE: media_type, + ATTR_MEDIA_CONTENT_ID: media_id} + + if entity_id: + data[ATTR_ENTITY_ID] = entity_id + + if enqueue: + data[ATTR_MEDIA_ENQUEUE] = enqueue + + hass.services.call(DOMAIN, SERVICE_PLAY_MEDIA, data) + + +@bind_hass +def select_source(hass, source, entity_id=None): + """Send the media player the command to select input source.""" + data = {ATTR_INPUT_SOURCE: source} + + if entity_id: + data[ATTR_ENTITY_ID] = entity_id + + hass.services.call(DOMAIN, SERVICE_SELECT_SOURCE, data) + + +@bind_hass +def clear_playlist(hass, entity_id=None): + """Send the media player the command for clear playlist.""" + data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} + hass.services.call(DOMAIN, SERVICE_CLEAR_PLAYLIST, data) diff --git a/tests/components/media_player/test_demo.py b/tests/components/media_player/test_demo.py index 121018e7541..e986ac02065 100644 --- a/tests/components/media_player/test_demo.py +++ b/tests/components/media_player/test_demo.py @@ -12,6 +12,7 @@ from homeassistant.helpers.aiohttp_client import DATA_CLIENTSESSION import requests from tests.common import get_test_home_assistant, get_test_instance_port +from tests.components.media_player import common SERVER_PORT = get_test_instance_port() HTTP_BASE_URL = 'http://127.0.0.1:{}'.format(SERVER_PORT) @@ -42,12 +43,12 @@ class TestDemoMediaPlayer(unittest.TestCase): state = self.hass.states.get(entity_id) assert 'dvd' == state.attributes.get('source') - mp.select_source(self.hass, None, entity_id) + common.select_source(self.hass, None, entity_id) self.hass.block_till_done() state = self.hass.states.get(entity_id) assert 'dvd' == state.attributes.get('source') - mp.select_source(self.hass, 'xbox', entity_id) + common.select_source(self.hass, 'xbox', entity_id) self.hass.block_till_done() state = self.hass.states.get(entity_id) assert 'xbox' == state.attributes.get('source') @@ -59,7 +60,7 @@ class TestDemoMediaPlayer(unittest.TestCase): {'media_player': {'platform': 'demo'}}) assert self.hass.states.is_state(entity_id, 'playing') - mp.clear_playlist(self.hass, entity_id) + common.clear_playlist(self.hass, entity_id) self.hass.block_till_done() assert self.hass.states.is_state(entity_id, 'off') @@ -71,34 +72,34 @@ class TestDemoMediaPlayer(unittest.TestCase): state = self.hass.states.get(entity_id) assert 1.0 == state.attributes.get('volume_level') - mp.set_volume_level(self.hass, None, entity_id) + common.set_volume_level(self.hass, None, entity_id) self.hass.block_till_done() state = self.hass.states.get(entity_id) assert 1.0 == state.attributes.get('volume_level') - mp.set_volume_level(self.hass, 0.5, entity_id) + common.set_volume_level(self.hass, 0.5, entity_id) self.hass.block_till_done() state = self.hass.states.get(entity_id) assert 0.5 == state.attributes.get('volume_level') - mp.volume_down(self.hass, entity_id) + common.volume_down(self.hass, entity_id) self.hass.block_till_done() state = self.hass.states.get(entity_id) assert 0.4 == state.attributes.get('volume_level') - mp.volume_up(self.hass, entity_id) + common.volume_up(self.hass, entity_id) self.hass.block_till_done() state = self.hass.states.get(entity_id) assert 0.5 == state.attributes.get('volume_level') assert False is state.attributes.get('is_volume_muted') - mp.mute_volume(self.hass, None, entity_id) + common.mute_volume(self.hass, None, entity_id) self.hass.block_till_done() state = self.hass.states.get(entity_id) assert False is state.attributes.get('is_volume_muted') - mp.mute_volume(self.hass, True, entity_id) + common.mute_volume(self.hass, True, entity_id) self.hass.block_till_done() state = self.hass.states.get(entity_id) assert True is state.attributes.get('is_volume_muted') @@ -110,16 +111,16 @@ class TestDemoMediaPlayer(unittest.TestCase): {'media_player': {'platform': 'demo'}}) assert self.hass.states.is_state(entity_id, 'playing') - mp.turn_off(self.hass, entity_id) + common.turn_off(self.hass, entity_id) self.hass.block_till_done() assert self.hass.states.is_state(entity_id, 'off') assert not mp.is_on(self.hass, entity_id) - mp.turn_on(self.hass, entity_id) + common.turn_on(self.hass, entity_id) self.hass.block_till_done() assert self.hass.states.is_state(entity_id, 'playing') - mp.toggle(self.hass, entity_id) + common.toggle(self.hass, entity_id) self.hass.block_till_done() assert self.hass.states.is_state(entity_id, 'off') assert not mp.is_on(self.hass, entity_id) @@ -131,19 +132,19 @@ class TestDemoMediaPlayer(unittest.TestCase): {'media_player': {'platform': 'demo'}}) assert self.hass.states.is_state(entity_id, 'playing') - mp.media_pause(self.hass, entity_id) + common.media_pause(self.hass, entity_id) self.hass.block_till_done() assert self.hass.states.is_state(entity_id, 'paused') - mp.media_play_pause(self.hass, entity_id) + common.media_play_pause(self.hass, entity_id) self.hass.block_till_done() assert self.hass.states.is_state(entity_id, 'playing') - mp.media_play_pause(self.hass, entity_id) + common.media_play_pause(self.hass, entity_id) self.hass.block_till_done() assert self.hass.states.is_state(entity_id, 'paused') - mp.media_play(self.hass, entity_id) + common.media_play(self.hass, entity_id) self.hass.block_till_done() assert self.hass.states.is_state(entity_id, 'playing') @@ -155,17 +156,17 @@ class TestDemoMediaPlayer(unittest.TestCase): state = self.hass.states.get(entity_id) assert 1 == state.attributes.get('media_track') - mp.media_next_track(self.hass, entity_id) + common.media_next_track(self.hass, entity_id) self.hass.block_till_done() state = self.hass.states.get(entity_id) assert 2 == state.attributes.get('media_track') - mp.media_next_track(self.hass, entity_id) + common.media_next_track(self.hass, entity_id) self.hass.block_till_done() state = self.hass.states.get(entity_id) assert 3 == state.attributes.get('media_track') - mp.media_previous_track(self.hass, entity_id) + common.media_previous_track(self.hass, entity_id) self.hass.block_till_done() state = self.hass.states.get(entity_id) assert 2 == state.attributes.get('media_track') @@ -177,12 +178,12 @@ class TestDemoMediaPlayer(unittest.TestCase): state = self.hass.states.get(ent_id) assert 1 == state.attributes.get('media_episode') - mp.media_next_track(self.hass, ent_id) + common.media_next_track(self.hass, ent_id) self.hass.block_till_done() state = self.hass.states.get(ent_id) assert 2 == state.attributes.get('media_episode') - mp.media_previous_track(self.hass, ent_id) + common.media_previous_track(self.hass, ent_id) self.hass.block_till_done() state = self.hass.states.get(ent_id) assert 1 == state.attributes.get('media_episode') @@ -200,14 +201,14 @@ class TestDemoMediaPlayer(unittest.TestCase): state.attributes.get('supported_features')) assert state.attributes.get('media_content_id') is not None - mp.play_media(self.hass, None, 'some_id', ent_id) + common.play_media(self.hass, None, 'some_id', ent_id) self.hass.block_till_done() state = self.hass.states.get(ent_id) assert 0 < (mp.SUPPORT_PLAY_MEDIA & state.attributes.get('supported_features')) assert not 'some_id' == state.attributes.get('media_content_id') - mp.play_media(self.hass, 'youtube', 'some_id', ent_id) + common.play_media(self.hass, 'youtube', 'some_id', ent_id) self.hass.block_till_done() state = self.hass.states.get(ent_id) assert 0 < (mp.SUPPORT_PLAY_MEDIA & @@ -215,10 +216,10 @@ class TestDemoMediaPlayer(unittest.TestCase): assert 'some_id' == state.attributes.get('media_content_id') assert not mock_seek.called - mp.media_seek(self.hass, None, ent_id) + common.media_seek(self.hass, None, ent_id) self.hass.block_till_done() assert not mock_seek.called - mp.media_seek(self.hass, 100, ent_id) + common.media_seek(self.hass, 100, ent_id) self.hass.block_till_done() assert mock_seek.called diff --git a/tests/components/notify/common.py b/tests/components/notify/common.py new file mode 100644 index 00000000000..42b4b35b63f --- /dev/null +++ b/tests/components/notify/common.py @@ -0,0 +1,24 @@ +"""Collection of helper methods. + +All containing methods are legacy helpers that should not be used by new +components. Instead call the service directly. +""" +from homeassistant.components.notify import ( + ATTR_DATA, ATTR_MESSAGE, ATTR_TITLE, DOMAIN, SERVICE_NOTIFY) +from homeassistant.loader import bind_hass + + +@bind_hass +def send_message(hass, message, title=None, data=None): + """Send a notification message.""" + info = { + ATTR_MESSAGE: message + } + + if title is not None: + info[ATTR_TITLE] = title + + if data is not None: + info[ATTR_DATA] = data + + hass.services.call(DOMAIN, SERVICE_NOTIFY, info) diff --git a/tests/components/notify/test_demo.py b/tests/components/notify/test_demo.py index f9c107a447e..3be2ddcb86b 100644 --- a/tests/components/notify/test_demo.py +++ b/tests/components/notify/test_demo.py @@ -7,7 +7,9 @@ from homeassistant.setup import setup_component from homeassistant.components.notify import demo from homeassistant.core import callback from homeassistant.helpers import discovery, script + from tests.common import assert_setup_component, get_test_home_assistant +from tests.components.notify import common CONFIG = { notify.DOMAIN: { @@ -79,7 +81,7 @@ class TestNotifyDemo(unittest.TestCase): def test_sending_none_message(self): """Test send with None as message.""" self._setup_notify() - notify.send_message(self.hass, None) + common.send_message(self.hass, None) self.hass.block_till_done() self.assertTrue(len(self.events) == 0) @@ -87,7 +89,7 @@ class TestNotifyDemo(unittest.TestCase): """Send a templated message.""" self._setup_notify() self.hass.states.set('sensor.temperature', 10) - notify.send_message(self.hass, '{{ states.sensor.temperature.state }}', + common.send_message(self.hass, '{{ states.sensor.temperature.state }}', '{{ states.sensor.temperature.name }}') self.hass.block_till_done() last_event = self.events[-1] @@ -97,7 +99,7 @@ class TestNotifyDemo(unittest.TestCase): def test_method_forwards_correct_data(self): """Test that all data from the service gets forwarded to service.""" self._setup_notify() - notify.send_message(self.hass, 'my message', 'my title', + common.send_message(self.hass, 'my message', 'my title', {'hello': 'world'}) self.hass.block_till_done() self.assertTrue(len(self.events) == 1) diff --git a/tests/components/remote/common.py b/tests/components/remote/common.py new file mode 100644 index 00000000000..d03cf5d6d16 --- /dev/null +++ b/tests/components/remote/common.py @@ -0,0 +1,55 @@ +"""Collection of helper methods. + +All containing methods are legacy helpers that should not be used by new +components. Instead call the service directly. +""" +from homeassistant.components.remote import ( + ATTR_ACTIVITY, ATTR_COMMAND, ATTR_DELAY_SECS, ATTR_DEVICE, + ATTR_NUM_REPEATS, DOMAIN, SERVICE_SEND_COMMAND) +from homeassistant.const import ( + ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON) +from homeassistant.loader import bind_hass + + +@bind_hass +def turn_on(hass, activity=None, entity_id=None): + """Turn all or specified remote on.""" + data = { + key: value for key, value in [ + (ATTR_ACTIVITY, activity), + (ATTR_ENTITY_ID, entity_id), + ] if value is not None} + hass.services.call(DOMAIN, SERVICE_TURN_ON, data) + + +@bind_hass +def turn_off(hass, activity=None, entity_id=None): + """Turn all or specified remote off.""" + data = {} + if activity: + data[ATTR_ACTIVITY] = activity + + if entity_id: + data[ATTR_ENTITY_ID] = entity_id + + hass.services.call(DOMAIN, SERVICE_TURN_OFF, data) + + +@bind_hass +def send_command(hass, command, entity_id=None, device=None, + num_repeats=None, delay_secs=None): + """Send a command to a device.""" + data = {ATTR_COMMAND: command} + if entity_id: + data[ATTR_ENTITY_ID] = entity_id + + if device: + data[ATTR_DEVICE] = device + + if num_repeats: + data[ATTR_NUM_REPEATS] = num_repeats + + if delay_secs: + data[ATTR_DELAY_SECS] = delay_secs + + hass.services.call(DOMAIN, SERVICE_SEND_COMMAND, data) diff --git a/tests/components/remote/test_demo.py b/tests/components/remote/test_demo.py index a0290987ff2..fbf7230c237 100644 --- a/tests/components/remote/test_demo.py +++ b/tests/components/remote/test_demo.py @@ -5,7 +5,9 @@ import unittest from homeassistant.setup import setup_component import homeassistant.components.remote as remote from homeassistant.const import STATE_ON, STATE_OFF + from tests.common import get_test_home_assistant +from tests.components.remote import common ENTITY_ID = 'remote.remote_one' @@ -28,22 +30,22 @@ class TestDemoRemote(unittest.TestCase): def test_methods(self): """Test if services call the entity methods as expected.""" - remote.turn_on(self.hass, entity_id=ENTITY_ID) + common.turn_on(self.hass, entity_id=ENTITY_ID) self.hass.block_till_done() state = self.hass.states.get(ENTITY_ID) self.assertEqual(state.state, STATE_ON) - remote.turn_off(self.hass, entity_id=ENTITY_ID) + common.turn_off(self.hass, entity_id=ENTITY_ID) self.hass.block_till_done() state = self.hass.states.get(ENTITY_ID) self.assertEqual(state.state, STATE_OFF) - remote.turn_on(self.hass, entity_id=ENTITY_ID) + common.turn_on(self.hass, entity_id=ENTITY_ID) self.hass.block_till_done() state = self.hass.states.get(ENTITY_ID) self.assertEqual(state.state, STATE_ON) - remote.send_command(self.hass, 'test', entity_id=ENTITY_ID) + common.send_command(self.hass, 'test', entity_id=ENTITY_ID) self.hass.block_till_done() state = self.hass.states.get(ENTITY_ID) self.assertEqual( diff --git a/tests/components/remote/test_init.py b/tests/components/remote/test_init.py index d98ec941f8b..21a083e3b9a 100644 --- a/tests/components/remote/test_init.py +++ b/tests/components/remote/test_init.py @@ -10,6 +10,8 @@ from homeassistant.const import ( import homeassistant.components.remote as remote from tests.common import mock_service, get_test_home_assistant +from tests.components.remote import common + TEST_PLATFORM = {remote.DOMAIN: {CONF_PLATFORM: 'test'}} SERVICE_SEND_COMMAND = 'send_command' @@ -46,7 +48,7 @@ class TestRemote(unittest.TestCase): turn_on_calls = mock_service( self.hass, remote.DOMAIN, SERVICE_TURN_ON) - remote.turn_on( + common.turn_on( self.hass, entity_id='entity_id_val') @@ -62,7 +64,7 @@ class TestRemote(unittest.TestCase): turn_off_calls = mock_service( self.hass, remote.DOMAIN, SERVICE_TURN_OFF) - remote.turn_off( + common.turn_off( self.hass, entity_id='entity_id_val') self.hass.block_till_done() @@ -79,7 +81,7 @@ class TestRemote(unittest.TestCase): send_command_calls = mock_service( self.hass, remote.DOMAIN, SERVICE_SEND_COMMAND) - remote.send_command( + common.send_command( self.hass, entity_id='entity_id_val', device='test_device', command=['test_command'], num_repeats='4', delay_secs='0.6') diff --git a/tests/components/scene/common.py b/tests/components/scene/common.py new file mode 100644 index 00000000000..4f8123ca638 --- /dev/null +++ b/tests/components/scene/common.py @@ -0,0 +1,19 @@ +"""Collection of helper methods. + +All containing methods are legacy helpers that should not be used by new +components. Instead call the service directly. +""" +from homeassistant.components.scene import DOMAIN +from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_ON +from homeassistant.loader import bind_hass + + +@bind_hass +def activate(hass, entity_id=None): + """Activate a scene.""" + data = {} + + if entity_id: + data[ATTR_ENTITY_ID] = entity_id + + hass.services.call(DOMAIN, SERVICE_TURN_ON, data) diff --git a/tests/components/scene/test_init.py b/tests/components/scene/test_init.py index 3298d7648d9..08d9af95275 100644 --- a/tests/components/scene/test_init.py +++ b/tests/components/scene/test_init.py @@ -8,6 +8,7 @@ from homeassistant.components import light, scene from homeassistant.util import yaml from tests.common import get_test_home_assistant +from tests.components.scene import common class TestScene(unittest.TestCase): @@ -68,7 +69,7 @@ class TestScene(unittest.TestCase): }] })) - scene.activate(self.hass, 'scene.test') + common.activate(self.hass, 'scene.test') self.hass.block_till_done() self.assertTrue(self.light_1.is_on) @@ -94,7 +95,7 @@ class TestScene(unittest.TestCase): doc = yaml.yaml.safe_load(file) self.assertTrue(setup_component(self.hass, scene.DOMAIN, doc)) - scene.activate(self.hass, 'scene.test') + common.activate(self.hass, 'scene.test') self.hass.block_till_done() self.assertTrue(self.light_1.is_on) @@ -117,7 +118,7 @@ class TestScene(unittest.TestCase): }] })) - scene.activate(self.hass, 'scene.test') + common.activate(self.hass, 'scene.test') self.hass.block_till_done() self.assertTrue(self.light_1.is_on) diff --git a/tests/components/scene/test_litejet.py b/tests/components/scene/test_litejet.py index 864ffc41735..57d3c178dbd 100644 --- a/tests/components/scene/test_litejet.py +++ b/tests/components/scene/test_litejet.py @@ -5,8 +5,9 @@ from unittest import mock from homeassistant import setup from homeassistant.components import litejet + from tests.common import get_test_home_assistant -import homeassistant.components.scene as scene +from tests.components.scene import common _LOGGER = logging.getLogger(__name__) @@ -59,7 +60,7 @@ class TestLiteJetScene(unittest.TestCase): def test_activate(self): """Test activating the scene.""" - scene.activate(self.hass, ENTITY_SCENE) + common.activate(self.hass, ENTITY_SCENE) self.hass.block_till_done() self.mock_lj.activate_scene.assert_called_once_with( ENTITY_SCENE_NUMBER) diff --git a/tests/components/vacuum/common.py b/tests/components/vacuum/common.py new file mode 100644 index 00000000000..436f23f5546 --- /dev/null +++ b/tests/components/vacuum/common.py @@ -0,0 +1,101 @@ +"""Collection of helper methods. + +All containing methods are legacy helpers that should not be used by new +components. Instead call the service directly. +""" +from homeassistant.components.vacuum import ( + ATTR_FAN_SPEED, ATTR_PARAMS, DOMAIN, SERVICE_CLEAN_SPOT, SERVICE_LOCATE, + SERVICE_PAUSE, SERVICE_SEND_COMMAND, SERVICE_SET_FAN_SPEED, SERVICE_START, + SERVICE_START_PAUSE, SERVICE_STOP, SERVICE_RETURN_TO_BASE) +from homeassistant.const import ( + ATTR_COMMAND, ATTR_ENTITY_ID, SERVICE_TOGGLE, + SERVICE_TURN_OFF, SERVICE_TURN_ON) +from homeassistant.loader import bind_hass + + +@bind_hass +def 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) + + +@bind_hass +def 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) + + +@bind_hass +def 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) + + +@bind_hass +def 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) + + +@bind_hass +def 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) + + +@bind_hass +def 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) + + +@bind_hass +def 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) + + +@bind_hass +def 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) + + +@bind_hass +def 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) + + +@bind_hass +def 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) + + +@bind_hass +def 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) + + +@bind_hass +def 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) diff --git a/tests/components/vacuum/test_demo.py b/tests/components/vacuum/test_demo.py index 1fc8f8cd5c1..f88908ecc41 100644 --- a/tests/components/vacuum/test_demo.py +++ b/tests/components/vacuum/test_demo.py @@ -15,7 +15,9 @@ from homeassistant.components.vacuum.demo import ( from homeassistant.const import ( ATTR_SUPPORTED_FEATURES, CONF_PLATFORM, STATE_OFF, STATE_ON) from homeassistant.setup import setup_component + from tests.common import get_test_home_assistant, mock_service +from tests.components.vacuum import common ENTITY_VACUUM_BASIC = '{}.{}'.format(DOMAIN, DEMO_VACUUM_BASIC).lower() @@ -108,27 +110,27 @@ class TestVacuumDemo(unittest.TestCase): self.hass.block_till_done() self.assertFalse(vacuum.is_on(self.hass)) - vacuum.turn_on(self.hass, ENTITY_VACUUM_COMPLETE) + common.turn_on(self.hass, ENTITY_VACUUM_COMPLETE) self.hass.block_till_done() self.assertTrue(vacuum.is_on(self.hass, ENTITY_VACUUM_COMPLETE)) - vacuum.turn_off(self.hass, ENTITY_VACUUM_COMPLETE) + common.turn_off(self.hass, ENTITY_VACUUM_COMPLETE) self.hass.block_till_done() self.assertFalse(vacuum.is_on(self.hass, ENTITY_VACUUM_COMPLETE)) - vacuum.toggle(self.hass, ENTITY_VACUUM_COMPLETE) + common.toggle(self.hass, ENTITY_VACUUM_COMPLETE) self.hass.block_till_done() self.assertTrue(vacuum.is_on(self.hass, ENTITY_VACUUM_COMPLETE)) - vacuum.start_pause(self.hass, ENTITY_VACUUM_COMPLETE) + common.start_pause(self.hass, ENTITY_VACUUM_COMPLETE) self.hass.block_till_done() self.assertFalse(vacuum.is_on(self.hass, ENTITY_VACUUM_COMPLETE)) - vacuum.start_pause(self.hass, ENTITY_VACUUM_COMPLETE) + common.start_pause(self.hass, ENTITY_VACUUM_COMPLETE) self.hass.block_till_done() self.assertTrue(vacuum.is_on(self.hass, ENTITY_VACUUM_COMPLETE)) - vacuum.stop(self.hass, ENTITY_VACUUM_COMPLETE) + common.stop(self.hass, ENTITY_VACUUM_COMPLETE) self.hass.block_till_done() self.assertFalse(vacuum.is_on(self.hass, ENTITY_VACUUM_COMPLETE)) @@ -136,39 +138,39 @@ class TestVacuumDemo(unittest.TestCase): self.assertLess(state.attributes.get(ATTR_BATTERY_LEVEL), 100) self.assertNotEqual("Charging", state.attributes.get(ATTR_STATUS)) - vacuum.locate(self.hass, ENTITY_VACUUM_COMPLETE) + common.locate(self.hass, ENTITY_VACUUM_COMPLETE) self.hass.block_till_done() state = self.hass.states.get(ENTITY_VACUUM_COMPLETE) self.assertIn("I'm over here", state.attributes.get(ATTR_STATUS)) - vacuum.return_to_base(self.hass, ENTITY_VACUUM_COMPLETE) + common.return_to_base(self.hass, ENTITY_VACUUM_COMPLETE) self.hass.block_till_done() state = self.hass.states.get(ENTITY_VACUUM_COMPLETE) self.assertIn("Returning home", state.attributes.get(ATTR_STATUS)) - vacuum.set_fan_speed(self.hass, FAN_SPEEDS[-1], + common.set_fan_speed(self.hass, FAN_SPEEDS[-1], entity_id=ENTITY_VACUUM_COMPLETE) self.hass.block_till_done() state = self.hass.states.get(ENTITY_VACUUM_COMPLETE) self.assertEqual(FAN_SPEEDS[-1], state.attributes.get(ATTR_FAN_SPEED)) - vacuum.clean_spot(self.hass, entity_id=ENTITY_VACUUM_COMPLETE) + common.clean_spot(self.hass, entity_id=ENTITY_VACUUM_COMPLETE) self.hass.block_till_done() state = self.hass.states.get(ENTITY_VACUUM_COMPLETE) self.assertIn("spot", state.attributes.get(ATTR_STATUS)) self.assertEqual(STATE_ON, state.state) - vacuum.start(self.hass, ENTITY_VACUUM_STATE) + common.start(self.hass, ENTITY_VACUUM_STATE) self.hass.block_till_done() state = self.hass.states.get(ENTITY_VACUUM_STATE) self.assertEqual(STATE_CLEANING, state.state) - vacuum.pause(self.hass, ENTITY_VACUUM_STATE) + common.pause(self.hass, ENTITY_VACUUM_STATE) self.hass.block_till_done() state = self.hass.states.get(ENTITY_VACUUM_STATE) self.assertEqual(STATE_PAUSED, state.state) - vacuum.stop(self.hass, ENTITY_VACUUM_STATE) + common.stop(self.hass, ENTITY_VACUUM_STATE) self.hass.block_till_done() state = self.hass.states.get(ENTITY_VACUUM_STATE) self.assertEqual(STATE_IDLE, state.state) @@ -177,18 +179,18 @@ class TestVacuumDemo(unittest.TestCase): self.assertLess(state.attributes.get(ATTR_BATTERY_LEVEL), 100) self.assertNotEqual(STATE_DOCKED, state.state) - vacuum.return_to_base(self.hass, ENTITY_VACUUM_STATE) + common.return_to_base(self.hass, ENTITY_VACUUM_STATE) self.hass.block_till_done() state = self.hass.states.get(ENTITY_VACUUM_STATE) self.assertEqual(STATE_RETURNING, state.state) - vacuum.set_fan_speed(self.hass, FAN_SPEEDS[-1], + common.set_fan_speed(self.hass, FAN_SPEEDS[-1], entity_id=ENTITY_VACUUM_STATE) self.hass.block_till_done() state = self.hass.states.get(ENTITY_VACUUM_STATE) self.assertEqual(FAN_SPEEDS[-1], state.attributes.get(ATTR_FAN_SPEED)) - vacuum.clean_spot(self.hass, entity_id=ENTITY_VACUUM_STATE) + common.clean_spot(self.hass, entity_id=ENTITY_VACUUM_STATE) self.hass.block_till_done() state = self.hass.states.get(ENTITY_VACUUM_STATE) self.assertEqual(STATE_CLEANING, state.state) @@ -199,11 +201,11 @@ class TestVacuumDemo(unittest.TestCase): self.hass.block_till_done() self.assertTrue(vacuum.is_on(self.hass, ENTITY_VACUUM_NONE)) - vacuum.turn_off(self.hass, ENTITY_VACUUM_NONE) + common.turn_off(self.hass, ENTITY_VACUUM_NONE) self.hass.block_till_done() self.assertTrue(vacuum.is_on(self.hass, ENTITY_VACUUM_NONE)) - vacuum.stop(self.hass, ENTITY_VACUUM_NONE) + common.stop(self.hass, ENTITY_VACUUM_NONE) self.hass.block_till_done() self.assertTrue(vacuum.is_on(self.hass, ENTITY_VACUUM_NONE)) @@ -211,37 +213,37 @@ class TestVacuumDemo(unittest.TestCase): self.hass.block_till_done() self.assertFalse(vacuum.is_on(self.hass, ENTITY_VACUUM_NONE)) - vacuum.turn_on(self.hass, ENTITY_VACUUM_NONE) + common.turn_on(self.hass, ENTITY_VACUUM_NONE) self.hass.block_till_done() self.assertFalse(vacuum.is_on(self.hass, ENTITY_VACUUM_NONE)) - vacuum.toggle(self.hass, ENTITY_VACUUM_NONE) + common.toggle(self.hass, ENTITY_VACUUM_NONE) self.hass.block_till_done() self.assertFalse(vacuum.is_on(self.hass, ENTITY_VACUUM_NONE)) # Non supported methods: - vacuum.start_pause(self.hass, ENTITY_VACUUM_NONE) + common.start_pause(self.hass, ENTITY_VACUUM_NONE) self.hass.block_till_done() self.assertFalse(vacuum.is_on(self.hass, ENTITY_VACUUM_NONE)) - vacuum.locate(self.hass, ENTITY_VACUUM_NONE) + common.locate(self.hass, ENTITY_VACUUM_NONE) self.hass.block_till_done() state = self.hass.states.get(ENTITY_VACUUM_NONE) self.assertIsNone(state.attributes.get(ATTR_STATUS)) - vacuum.return_to_base(self.hass, ENTITY_VACUUM_NONE) + common.return_to_base(self.hass, ENTITY_VACUUM_NONE) self.hass.block_till_done() state = self.hass.states.get(ENTITY_VACUUM_NONE) self.assertIsNone(state.attributes.get(ATTR_STATUS)) - vacuum.set_fan_speed(self.hass, FAN_SPEEDS[-1], + common.set_fan_speed(self.hass, FAN_SPEEDS[-1], entity_id=ENTITY_VACUUM_NONE) self.hass.block_till_done() state = self.hass.states.get(ENTITY_VACUUM_NONE) self.assertNotEqual(FAN_SPEEDS[-1], state.attributes.get(ATTR_FAN_SPEED)) - vacuum.clean_spot(self.hass, entity_id=ENTITY_VACUUM_BASIC) + common.clean_spot(self.hass, entity_id=ENTITY_VACUUM_BASIC) self.hass.block_till_done() state = self.hass.states.get(ENTITY_VACUUM_BASIC) self.assertNotIn("spot", state.attributes.get(ATTR_STATUS)) @@ -252,7 +254,7 @@ class TestVacuumDemo(unittest.TestCase): self.hass.block_till_done() self.assertTrue(vacuum.is_on(self.hass, ENTITY_VACUUM_COMPLETE)) - vacuum.pause(self.hass, ENTITY_VACUUM_COMPLETE) + common.pause(self.hass, ENTITY_VACUUM_COMPLETE) self.hass.block_till_done() self.assertTrue(vacuum.is_on(self.hass, ENTITY_VACUUM_COMPLETE)) @@ -260,22 +262,22 @@ class TestVacuumDemo(unittest.TestCase): self.hass.block_till_done() self.assertFalse(vacuum.is_on(self.hass, ENTITY_VACUUM_COMPLETE)) - vacuum.start(self.hass, ENTITY_VACUUM_COMPLETE) + common.start(self.hass, ENTITY_VACUUM_COMPLETE) self.hass.block_till_done() self.assertFalse(vacuum.is_on(self.hass, ENTITY_VACUUM_COMPLETE)) # StateVacuumDevice does not support on/off - vacuum.turn_on(self.hass, entity_id=ENTITY_VACUUM_STATE) + common.turn_on(self.hass, entity_id=ENTITY_VACUUM_STATE) self.hass.block_till_done() state = self.hass.states.get(ENTITY_VACUUM_STATE) self.assertNotEqual(STATE_CLEANING, state.state) - vacuum.turn_off(self.hass, entity_id=ENTITY_VACUUM_STATE) + common.turn_off(self.hass, entity_id=ENTITY_VACUUM_STATE) self.hass.block_till_done() state = self.hass.states.get(ENTITY_VACUUM_STATE) self.assertNotEqual(STATE_RETURNING, state.state) - vacuum.toggle(self.hass, entity_id=ENTITY_VACUUM_STATE) + common.toggle(self.hass, entity_id=ENTITY_VACUUM_STATE) self.hass.block_till_done() state = self.hass.states.get(ENTITY_VACUUM_STATE) self.assertNotEqual(STATE_CLEANING, state.state) @@ -287,7 +289,7 @@ class TestVacuumDemo(unittest.TestCase): self.hass, DOMAIN, SERVICE_SEND_COMMAND) params = {"rotate": 150, "speed": 20} - vacuum.send_command( + common.send_command( self.hass, 'test_command', entity_id=ENTITY_VACUUM_BASIC, params=params) @@ -305,7 +307,7 @@ class TestVacuumDemo(unittest.TestCase): set_fan_speed_calls = mock_service( self.hass, DOMAIN, SERVICE_SET_FAN_SPEED) - vacuum.set_fan_speed( + common.set_fan_speed( self.hass, FAN_SPEEDS[0], entity_id=ENTITY_VACUUM_COMPLETE) self.hass.block_till_done() @@ -326,7 +328,7 @@ class TestVacuumDemo(unittest.TestCase): old_state_complete = self.hass.states.get(ENTITY_VACUUM_COMPLETE) old_state_state = self.hass.states.get(ENTITY_VACUUM_STATE) - vacuum.set_fan_speed( + common.set_fan_speed( self.hass, FAN_SPEEDS[0], entity_id=group_vacuums) self.hass.block_till_done() @@ -356,7 +358,7 @@ class TestVacuumDemo(unittest.TestCase): old_state_basic = self.hass.states.get(ENTITY_VACUUM_BASIC) old_state_complete = self.hass.states.get(ENTITY_VACUUM_COMPLETE) - vacuum.send_command( + common.send_command( self.hass, 'test_command', params={"p1": 3}, entity_id=group_vacuums) diff --git a/tests/components/vacuum/test_mqtt.py b/tests/components/vacuum/test_mqtt.py index ba2288e3fc6..ddd4289c24d 100644 --- a/tests/components/vacuum/test_mqtt.py +++ b/tests/components/vacuum/test_mqtt.py @@ -9,8 +9,10 @@ from homeassistant.components.mqtt import CONF_COMMAND_TOPIC from homeassistant.const import ( CONF_PLATFORM, STATE_OFF, STATE_ON, STATE_UNAVAILABLE, CONF_NAME) from homeassistant.setup import setup_component + from tests.common import ( fire_mqtt_message, get_test_home_assistant, mock_mqtt_component) +from tests.components.vacuum import common class TestVacuumMQTT(unittest.TestCase): @@ -69,55 +71,55 @@ class TestVacuumMQTT(unittest.TestCase): vacuum.DOMAIN: self.default_config, })) - vacuum.turn_on(self.hass, 'vacuum.mqtttest') + 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() - vacuum.turn_off(self.hass, 'vacuum.mqtttest') + 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() - vacuum.stop(self.hass, 'vacuum.mqtttest') + 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() - vacuum.clean_spot(self.hass, 'vacuum.mqtttest') + 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() - vacuum.locate(self.hass, 'vacuum.mqtttest') + 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() - vacuum.start_pause(self.hass, 'vacuum.mqtttest') + 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() - vacuum.return_to_base(self.hass, 'vacuum.mqtttest') + 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() - vacuum.set_fan_speed(self.hass, 'high', 'vacuum.mqtttest') + 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() - vacuum.send_command(self.hass, '44 FE 93', entity_id='vacuum.mqtttest') + 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) From 4b674b1d16de47980391338f051dd37e3517f5b8 Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Wed, 26 Sep 2018 18:03:13 +0200 Subject: [PATCH 076/247] Remove unused legacy test helper methods (#16893) --- tests/components/camera/common.py | 23 +---------------------- tests/components/fan/common.py | 12 +----------- tests/components/test_input_select.py | 12 ------------ 3 files changed, 2 insertions(+), 45 deletions(-) diff --git a/tests/components/camera/common.py b/tests/components/camera/common.py index 71193f1a99d..21f7244bd29 100644 --- a/tests/components/camera/common.py +++ b/tests/components/camera/common.py @@ -4,20 +4,13 @@ All containing methods are legacy helpers that should not be used by new components. Instead call the service directly. """ from homeassistant.components.camera import ( - ATTR_FILENAME, DOMAIN, SERVICE_DISABLE_MOTION, SERVICE_ENABLE_MOTION, - SERVICE_SNAPSHOT) + ATTR_FILENAME, DOMAIN, SERVICE_ENABLE_MOTION, SERVICE_SNAPSHOT) from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, \ SERVICE_TURN_ON from homeassistant.core import callback from homeassistant.loader import bind_hass -@bind_hass -def turn_off(hass, entity_id=None): - """Turn off camera.""" - hass.add_job(async_turn_off, hass, entity_id) - - @bind_hass async def async_turn_off(hass, entity_id=None): """Turn off camera.""" @@ -25,12 +18,6 @@ async def async_turn_off(hass, entity_id=None): await hass.services.async_call(DOMAIN, SERVICE_TURN_OFF, data) -@bind_hass -def turn_on(hass, entity_id=None): - """Turn on camera.""" - hass.add_job(async_turn_on, hass, entity_id) - - @bind_hass async def async_turn_on(hass, entity_id=None): """Turn on camera, and set operation mode.""" @@ -49,14 +36,6 @@ def enable_motion_detection(hass, entity_id=None): DOMAIN, SERVICE_ENABLE_MOTION, data)) -@bind_hass -def disable_motion_detection(hass, entity_id=None): - """Disable Motion Detection.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else None - hass.async_add_job(hass.services.async_call( - DOMAIN, SERVICE_DISABLE_MOTION, data)) - - @bind_hass @callback def async_snapshot(hass, filename, entity_id=None): diff --git a/tests/components/fan/common.py b/tests/components/fan/common.py index d4caf28be5b..60e1cab1ac0 100644 --- a/tests/components/fan/common.py +++ b/tests/components/fan/common.py @@ -7,7 +7,7 @@ from homeassistant.components.fan import ( ATTR_DIRECTION, ATTR_SPEED, ATTR_OSCILLATING, DOMAIN, SERVICE_OSCILLATE, SERVICE_SET_DIRECTION, SERVICE_SET_SPEED) from homeassistant.const import ( - ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TOGGLE, SERVICE_TURN_OFF) + ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF) from homeassistant.loader import bind_hass @@ -32,16 +32,6 @@ def turn_off(hass, entity_id: str = None) -> None: hass.services.call(DOMAIN, SERVICE_TURN_OFF, data) -@bind_hass -def toggle(hass, entity_id: str = None) -> None: - """Toggle all or specified fans.""" - data = { - ATTR_ENTITY_ID: entity_id - } - - hass.services.call(DOMAIN, SERVICE_TOGGLE, data) - - @bind_hass def oscillate(hass, entity_id: str = None, should_oscillate: bool = True) -> None: diff --git a/tests/components/test_input_select.py b/tests/components/test_input_select.py index 938c93fa099..25f6d1c7673 100644 --- a/tests/components/test_input_select.py +++ b/tests/components/test_input_select.py @@ -49,18 +49,6 @@ def select_previous(hass, entity_id): }) -@bind_hass -def set_options(hass, entity_id, options): - """Set options of input_select. - - This is a legacy helper method. Do not use it for new tests. - """ - hass.services.call(DOMAIN, SERVICE_SET_OPTIONS, { - ATTR_ENTITY_ID: entity_id, - ATTR_OPTIONS: options, - }) - - class TestInputSelect(unittest.TestCase): """Test the input select component.""" From 273a7af3300302c4b842138ba813910248cf150e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 26 Sep 2018 18:03:25 +0200 Subject: [PATCH 077/247] Prevent discovered Tradfri while already configured (#16891) * Prevent discovered Tradfri while already configured * Lint --- homeassistant/components/tradfri/__init__.py | 12 +++- .../components/tradfri/config_flow.py | 6 ++ tests/components/tradfri/test_config_flow.py | 34 ++++++++- tests/components/tradfri/test_init.py | 72 +++++++++++++++++++ 4 files changed, 120 insertions(+), 4 deletions(-) create mode 100644 tests/components/tradfri/test_init.py diff --git a/homeassistant/components/tradfri/__init__.py b/homeassistant/components/tradfri/__init__.py index 079381f8b45..6e91ab338a3 100644 --- a/homeassistant/components/tradfri/__init__.py +++ b/homeassistant/components/tradfri/__init__.py @@ -44,10 +44,16 @@ async def async_setup(hass, config): if conf is None: return True - known_hosts = await hass.async_add_executor_job( + configured_hosts = [entry.data['host'] for entry in + hass.config_entries.async_entries(DOMAIN)] + + legacy_hosts = await hass.async_add_executor_job( load_json, hass.config.path(CONFIG_FILE)) - for host, info in known_hosts.items(): + for host, info in legacy_hosts.items(): + if host in configured_hosts: + continue + info[CONF_HOST] = host info[CONF_IMPORT_GROUPS] = conf[CONF_ALLOW_TRADFRI_GROUPS] @@ -58,7 +64,7 @@ async def async_setup(hass, config): host = conf.get(CONF_HOST) - if host is None or host in known_hosts: + if host is None or host in configured_hosts or host in legacy_hosts: return True hass.async_create_task(hass.config_entries.flow.async_init( diff --git a/homeassistant/components/tradfri/config_flow.py b/homeassistant/components/tradfri/config_flow.py index 8d8f9af79e6..29aa768dbb5 100644 --- a/homeassistant/components/tradfri/config_flow.py +++ b/homeassistant/components/tradfri/config_flow.py @@ -77,6 +77,12 @@ class FlowHandler(config_entries.ConfigFlow): async def async_step_discovery(self, user_input): """Handle discovery.""" + for entry in self._async_current_entries(): + if entry.data[CONF_HOST] == user_input['host']: + return self.async_abort( + reason='already_configured' + ) + self._host = user_input['host'] return await self.async_step_auth() diff --git a/tests/components/tradfri/test_config_flow.py b/tests/components/tradfri/test_config_flow.py index 580e9580d76..99566356f61 100644 --- a/tests/components/tradfri/test_config_flow.py +++ b/tests/components/tradfri/test_config_flow.py @@ -6,7 +6,7 @@ import pytest from homeassistant import data_entry_flow from homeassistant.components.tradfri import config_flow -from tests.common import mock_coro +from tests.common import mock_coro, MockConfigEntry @pytest.fixture @@ -185,3 +185,35 @@ async def test_import_connection_legacy(hass, mock_gateway_info, assert len(mock_gateway_info.mock_calls) == 1 assert len(mock_entry_setup.mock_calls) == 1 + + +async def test_discovery_duplicate_aborted(hass): + """Test a duplicate discovery host is ignored.""" + MockConfigEntry( + domain='tradfri', + data={'host': 'some-host'} + ).add_to_hass(hass) + + flow = await hass.config_entries.flow.async_init( + 'tradfri', context={'source': 'discovery'}, data={ + 'host': 'some-host' + }) + + assert flow['type'] == data_entry_flow.RESULT_TYPE_ABORT + assert flow['reason'] == 'already_configured' + + +async def test_import_duplicate_aborted(hass): + """Test a duplicate discovery host is ignored.""" + MockConfigEntry( + domain='tradfri', + data={'host': 'some-host'} + ).add_to_hass(hass) + + flow = await hass.config_entries.flow.async_init( + 'tradfri', context={'source': 'import'}, data={ + 'host': 'some-host' + }) + + assert flow['type'] == data_entry_flow.RESULT_TYPE_ABORT + assert flow['reason'] == 'already_configured' diff --git a/tests/components/tradfri/test_init.py b/tests/components/tradfri/test_init.py new file mode 100644 index 00000000000..4527e87f605 --- /dev/null +++ b/tests/components/tradfri/test_init.py @@ -0,0 +1,72 @@ +"""Tests for Tradfri setup.""" +from unittest.mock import patch + +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry + + +async def test_config_yaml_host_not_imported(hass): + """Test that we don't import a configured host.""" + MockConfigEntry( + domain='tradfri', + data={'host': 'mock-host'} + ).add_to_hass(hass) + + with patch('homeassistant.components.tradfri.load_json', + return_value={}), \ + patch.object(hass.config_entries.flow, 'async_init') as mock_init: + assert await async_setup_component(hass, 'tradfri', { + 'tradfri': { + 'host': 'mock-host' + } + }) + + assert len(mock_init.mock_calls) == 0 + + +async def test_config_yaml_host_imported(hass): + """Test that we import a configured host.""" + with patch('homeassistant.components.tradfri.load_json', + return_value={}): + assert await async_setup_component(hass, 'tradfri', { + 'tradfri': { + 'host': 'mock-host' + } + }) + + progress = hass.config_entries.flow.async_progress() + assert len(progress) == 1 + assert progress[0]['handler'] == 'tradfri' + assert progress[0]['context'] == {'source': 'import'} + + +async def test_config_json_host_not_imported(hass): + """Test that we don't import a configured host.""" + MockConfigEntry( + domain='tradfri', + data={'host': 'mock-host'} + ).add_to_hass(hass) + + with patch('homeassistant.components.tradfri.load_json', + return_value={'mock-host': {'key': 'some-info'}}), \ + patch.object(hass.config_entries.flow, 'async_init') as mock_init: + assert await async_setup_component(hass, 'tradfri', { + 'tradfri': {} + }) + + assert len(mock_init.mock_calls) == 0 + + +async def test_config_json_host_imported(hass): + """Test that we import a configured host.""" + with patch('homeassistant.components.tradfri.load_json', + return_value={'mock-host': {'key': 'some-info'}}): + assert await async_setup_component(hass, 'tradfri', { + 'tradfri': {} + }) + + progress = hass.config_entries.flow.async_progress() + assert len(progress) == 1 + assert progress[0]['handler'] == 'tradfri' + assert progress[0]['context'] == {'source': 'import'} From 631ecf578e9f4b6ab1166b2a8a659b8602096572 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 26 Sep 2018 23:48:55 +0200 Subject: [PATCH 078/247] Lint --- homeassistant/util/json.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/util/json.py b/homeassistant/util/json.py index d6f7e149040..40e26f80e91 100644 --- a/homeassistant/util/json.py +++ b/homeassistant/util/json.py @@ -3,7 +3,6 @@ import logging from typing import Union, List, Dict import json -import os from homeassistant.exceptions import HomeAssistantError From 726cf9b1c4417f44777268b46289243e55ebc8d9 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Thu, 27 Sep 2018 00:21:29 +0200 Subject: [PATCH 079/247] Remove unused import (#16909) * Remove unused import * Revert test usage --- tests/components/emulated_hue/test_init.py | 91 +++++++++++----------- 1 file changed, 44 insertions(+), 47 deletions(-) diff --git a/tests/components/emulated_hue/test_init.py b/tests/components/emulated_hue/test_init.py index d416dd08e05..2f443eb5d6e 100644 --- a/tests/components/emulated_hue/test_init.py +++ b/tests/components/emulated_hue/test_init.py @@ -16,25 +16,24 @@ def test_config_google_home_entity_id_to_number(): handle = mop() with patch('homeassistant.util.json.open', mop, create=True): - with patch('homeassistant.util.json.os.open', return_value=0): - number = conf.entity_id_to_number('light.test') - assert number == '2' - assert handle.write.call_count == 1 - assert json.loads(handle.write.mock_calls[0][1][0]) == { - '1': 'light.test2', - '2': 'light.test', - } + number = conf.entity_id_to_number('light.test') + assert number == '2' + assert handle.write.call_count == 1 + assert json.loads(handle.write.mock_calls[0][1][0]) == { + '1': 'light.test2', + '2': 'light.test', + } - number = conf.entity_id_to_number('light.test') - assert number == '2' - assert handle.write.call_count == 1 + number = conf.entity_id_to_number('light.test') + assert number == '2' + assert handle.write.call_count == 1 - number = conf.entity_id_to_number('light.test2') - assert number == '1' - assert handle.write.call_count == 1 + number = conf.entity_id_to_number('light.test2') + assert number == '1' + assert handle.write.call_count == 1 - entity_id = conf.number_to_entity_id('1') - assert entity_id == 'light.test2' + entity_id = conf.number_to_entity_id('1') + assert entity_id == 'light.test2' def test_config_google_home_entity_id_to_number_altered(): @@ -47,25 +46,24 @@ def test_config_google_home_entity_id_to_number_altered(): handle = mop() with patch('homeassistant.util.json.open', mop, create=True): - with patch('homeassistant.util.json.os.open', return_value=0): - number = conf.entity_id_to_number('light.test') - assert number == '22' - assert handle.write.call_count == 1 - assert json.loads(handle.write.mock_calls[0][1][0]) == { - '21': 'light.test2', - '22': 'light.test', - } + number = conf.entity_id_to_number('light.test') + assert number == '22' + assert handle.write.call_count == 1 + assert json.loads(handle.write.mock_calls[0][1][0]) == { + '21': 'light.test2', + '22': 'light.test', + } - number = conf.entity_id_to_number('light.test') - assert number == '22' - assert handle.write.call_count == 1 + number = conf.entity_id_to_number('light.test') + assert number == '22' + assert handle.write.call_count == 1 - number = conf.entity_id_to_number('light.test2') - assert number == '21' - assert handle.write.call_count == 1 + number = conf.entity_id_to_number('light.test2') + assert number == '21' + assert handle.write.call_count == 1 - entity_id = conf.number_to_entity_id('21') - assert entity_id == 'light.test2' + entity_id = conf.number_to_entity_id('21') + assert entity_id == 'light.test2' def test_config_google_home_entity_id_to_number_empty(): @@ -78,24 +76,23 @@ def test_config_google_home_entity_id_to_number_empty(): handle = mop() with patch('homeassistant.util.json.open', mop, create=True): - with patch('homeassistant.util.json.os.open', return_value=0): - number = conf.entity_id_to_number('light.test') - assert number == '1' - assert handle.write.call_count == 1 - assert json.loads(handle.write.mock_calls[0][1][0]) == { - '1': 'light.test', - } + number = conf.entity_id_to_number('light.test') + assert number == '1' + assert handle.write.call_count == 1 + assert json.loads(handle.write.mock_calls[0][1][0]) == { + '1': 'light.test', + } - number = conf.entity_id_to_number('light.test') - assert number == '1' - assert handle.write.call_count == 1 + number = conf.entity_id_to_number('light.test') + assert number == '1' + assert handle.write.call_count == 1 - number = conf.entity_id_to_number('light.test2') - assert number == '2' - assert handle.write.call_count == 2 + number = conf.entity_id_to_number('light.test2') + assert number == '2' + assert handle.write.call_count == 2 - entity_id = conf.number_to_entity_id('2') - assert entity_id == 'light.test2' + entity_id = conf.number_to_entity_id('2') + assert entity_id == 'light.test2' def test_config_alexa_entity_id_to_number(): From 96e5acda1a48cfc581f90c2293378afb2feca8b0 Mon Sep 17 00:00:00 2001 From: Jason Hu Date: Wed, 26 Sep 2018 15:38:13 -0700 Subject: [PATCH 080/247] Optimize Ring Sensors platform setup (#16886) --- homeassistant/components/binary_sensor/ring.py | 7 ++++--- homeassistant/components/sensor/ring.py | 10 ++++++---- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/binary_sensor/ring.py b/homeassistant/components/binary_sensor/ring.py index 102e22cbe2d..3945eb5c926 100644 --- a/homeassistant/components/binary_sensor/ring.py +++ b/homeassistant/components/binary_sensor/ring.py @@ -44,14 +44,15 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ring = hass.data[DATA_RING] sensors = [] - for sensor_type in config.get(CONF_MONITORED_CONDITIONS): - for device in ring.doorbells: + for device in ring.doorbells: # ring.doorbells is doing I/O + for sensor_type in config[CONF_MONITORED_CONDITIONS]: if 'doorbell' in SENSOR_TYPES[sensor_type][1]: sensors.append(RingBinarySensor(hass, device, sensor_type)) - for device in ring.stickup_cams: + for device in ring.stickup_cams: # ring.stickup_cams is doing I/O + for sensor_type in config[CONF_MONITORED_CONDITIONS]: if 'stickup_cams' in SENSOR_TYPES[sensor_type][1]: sensors.append(RingBinarySensor(hass, device, diff --git a/homeassistant/components/sensor/ring.py b/homeassistant/components/sensor/ring.py index 31c0360cc23..408971c60e1 100644 --- a/homeassistant/components/sensor/ring.py +++ b/homeassistant/components/sensor/ring.py @@ -66,16 +66,18 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ring = hass.data[DATA_RING] sensors = [] - for sensor_type in config.get(CONF_MONITORED_CONDITIONS): - for device in ring.chimes: + for device in ring.chimes: # ring.chimes is doing I/O + for sensor_type in config[CONF_MONITORED_CONDITIONS]: if 'chime' in SENSOR_TYPES[sensor_type][1]: sensors.append(RingSensor(hass, device, sensor_type)) - for device in ring.doorbells: + for device in ring.doorbells: # ring.doorbells is doing I/O + for sensor_type in config[CONF_MONITORED_CONDITIONS]: if 'doorbell' in SENSOR_TYPES[sensor_type][1]: sensors.append(RingSensor(hass, device, sensor_type)) - for device in ring.stickup_cams: + for device in ring.stickup_cams: # ring.stickup_cams is doing I/O + for sensor_type in config[CONF_MONITORED_CONDITIONS]: if 'stickup_cams' in SENSOR_TYPES[sensor_type][1]: sensors.append(RingSensor(hass, device, sensor_type)) From 81d4338b939b6f320af590d6ef37fe6ed549983f Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Thu, 27 Sep 2018 01:08:20 +0200 Subject: [PATCH 081/247] Upgrade aiolifx_effects to 0.2.0 (#16900) --- homeassistant/components/light/lifx.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/light/lifx.py b/homeassistant/components/light/lifx.py index bea39354e1b..21a501fa6ea 100644 --- a/homeassistant/components/light/lifx.py +++ b/homeassistant/components/light/lifx.py @@ -30,7 +30,7 @@ import homeassistant.util.color as color_util _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['aiolifx==0.6.3', 'aiolifx_effects==0.1.2'] +REQUIREMENTS = ['aiolifx==0.6.3', 'aiolifx_effects==0.2.0'] UDP_BROADCAST_PORT = 56700 diff --git a/requirements_all.txt b/requirements_all.txt index 4600af0c542..b84c0601c02 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -110,7 +110,7 @@ aioimaplib==0.7.13 aiolifx==0.6.3 # homeassistant.components.light.lifx -aiolifx_effects==0.1.2 +aiolifx_effects==0.2.0 # homeassistant.components.scene.hunterdouglas_powerview aiopvapi==1.5.4 From 24e9c62fe76ea4eaa98321411c3aebdd36d45f67 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Thu, 27 Sep 2018 01:09:30 +0200 Subject: [PATCH 082/247] Upgrade pysonos to 0.0.3 (#16901) This version has downgraded/removed some logging that we then no longer have to hide. --- homeassistant/components/media_player/sonos.py | 8 -------- homeassistant/components/sonos/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 3 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/media_player/sonos.py b/homeassistant/components/media_player/sonos.py index fd735a5b830..1486d47e759 100644 --- a/homeassistant/components/media_player/sonos.py +++ b/homeassistant/components/media_player/sonos.py @@ -33,9 +33,7 @@ _LOGGER = logging.getLogger(__name__) # Quiet down pysonos logging to just actual problems. logging.getLogger('pysonos').setLevel(logging.WARNING) -logging.getLogger('pysonos.events').setLevel(logging.ERROR) logging.getLogger('pysonos.data_structures_entry').setLevel(logging.ERROR) -_SOCO_SERVICES_LOGGER = logging.getLogger('pysonos.services') SUPPORT_SONOS = SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE |\ SUPPORT_PLAY | SUPPORT_PAUSE | SUPPORT_STOP | SUPPORT_SELECT_SOURCE |\ @@ -289,10 +287,6 @@ def soco_error(errorcodes=None): """Wrap for all soco UPnP exception.""" from pysonos.exceptions import SoCoUPnPException, SoCoException - # Temporarily disable SoCo logging because it will log the - # UPnP exception otherwise - _SOCO_SERVICES_LOGGER.disabled = True - try: return funct(*args, **kwargs) except SoCoUPnPException as err: @@ -302,8 +296,6 @@ def soco_error(errorcodes=None): _LOGGER.error("Error on %s with %s", funct.__name__, err) except SoCoException as err: _LOGGER.error("Error on %s with %s", funct.__name__, err) - finally: - _SOCO_SERVICES_LOGGER.disabled = False return wrapper return decorator diff --git a/homeassistant/components/sonos/__init__.py b/homeassistant/components/sonos/__init__.py index b4565794844..b794fe607e6 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.2'] +REQUIREMENTS = ['pysonos==0.0.3'] async def async_setup(hass, config): diff --git a/requirements_all.txt b/requirements_all.txt index b84c0601c02..a954b2cf94f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1071,7 +1071,7 @@ pysma==0.2 pysnmp==4.4.5 # homeassistant.components.sonos -pysonos==0.0.2 +pysonos==0.0.3 # homeassistant.components.spc pyspcwebgw==0.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 691fc641607..f33b37e01d1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -169,7 +169,7 @@ pyotp==2.2.6 pyqwikswitch==0.8 # homeassistant.components.sonos -pysonos==0.0.2 +pysonos==0.0.3 # homeassistant.components.spc pyspcwebgw==0.4.0 From 29db43edb2306c4abe2a976acb2ecaf115389d89 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Thu, 27 Sep 2018 09:44:19 +0200 Subject: [PATCH 083/247] Ignore Xiaomi hub callbacks during setup (#16910) --- homeassistant/components/xiaomi_aqara.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/xiaomi_aqara.py b/homeassistant/components/xiaomi_aqara.py index f2d51d2fc2e..9c2fb9f7fe7 100644 --- a/homeassistant/components/xiaomi_aqara.py +++ b/homeassistant/components/xiaomi_aqara.py @@ -218,7 +218,7 @@ class XiaomiDevice(Entity): self._get_from_hub = xiaomi_hub.get_from_hub self._device_state_attributes = {} self._remove_unavailability_tracker = None - xiaomi_hub.callbacks[self._sid].append(self._add_push_data_job) + self._xiaomi_hub = xiaomi_hub self.parse_data(device['data'], device['raw_data']) self.parse_voltage(device['data']) @@ -236,6 +236,7 @@ class XiaomiDevice(Entity): @asyncio.coroutine def async_added_to_hass(self): """Start unavailability tracking.""" + self._xiaomi_hub.callbacks[self._sid].append(self._add_push_data_job) self._async_track_unavailable() @property From da3342f1aaa71fb24df40046eb3de8ef884db209 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 27 Sep 2018 11:26:58 +0200 Subject: [PATCH 084/247] Update new values coming in for dev registry (#16852) * Update new values coming in for dev registry * fix Lint+Test;2C --- homeassistant/helpers/device_registry.py | 89 ++++++++++++------- .../components/config/test_device_registry.py | 1 - tests/helpers/test_device_registry.py | 15 +++- 3 files changed, 66 insertions(+), 39 deletions(-) diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index 8d4cd0a5bbf..478b29c75b2 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -26,11 +26,12 @@ CONNECTION_ZIGBEE = 'zigbee' class DeviceEntry: """Device Registry Entry.""" - config_entries = attr.ib(type=set, converter=set) - connections = attr.ib(type=set, converter=set) - identifiers = attr.ib(type=set, converter=set) - manufacturer = attr.ib(type=str) - model = attr.ib(type=str) + config_entries = attr.ib(type=set, converter=set, + default=attr.Factory(set)) + connections = attr.ib(type=set, converter=set, default=attr.Factory(set)) + identifiers = attr.ib(type=set, converter=set, default=attr.Factory(set)) + manufacturer = attr.ib(type=str, default=None) + model = attr.ib(type=str, default=None) name = attr.ib(type=str, default=None) sw_version = attr.ib(type=str, default=None) hub_device_id = attr.ib(type=str, default=None) @@ -56,46 +57,53 @@ class DeviceRegistry: return None @callback - def async_get_or_create(self, *, config_entry_id, connections, identifiers, - manufacturer, model, name=None, sw_version=None, + def async_get_or_create(self, *, config_entry_id, connections=None, + identifiers=None, manufacturer=_UNDEF, + model=_UNDEF, name=_UNDEF, sw_version=_UNDEF, via_hub=None): """Get device. Create if it doesn't exist.""" if not identifiers and not connections: return None + if identifiers is None: + identifiers = set() + + if connections is None: + connections = set() + device = self.async_get_device(identifiers, connections) + if device is None: + device = DeviceEntry() + self.devices[device.id] = device + if via_hub is not None: hub_device = self.async_get_device({via_hub}, set()) - hub_device_id = hub_device.id if hub_device else None + hub_device_id = hub_device.id if hub_device else _UNDEF else: - hub_device_id = None + hub_device_id = _UNDEF - if device is not None: - return self._async_update_device( - device.id, config_entry_id=config_entry_id, - hub_device_id=hub_device_id - ) - - device = DeviceEntry( - config_entries={config_entry_id}, - connections=connections, - identifiers=identifiers, + return self._async_update_device( + device.id, + add_config_entry_id=config_entry_id, + hub_device_id=hub_device_id, + merge_connections=connections, + merge_identifiers=identifiers, manufacturer=manufacturer, model=model, name=name, sw_version=sw_version, - hub_device_id=hub_device_id ) - self.devices[device.id] = device - - self.async_schedule_save() - - return device @callback - def _async_update_device(self, device_id, *, config_entry_id=_UNDEF, + def _async_update_device(self, device_id, *, add_config_entry_id=_UNDEF, remove_config_entry_id=_UNDEF, + merge_connections=_UNDEF, + merge_identifiers=_UNDEF, + manufacturer=_UNDEF, + model=_UNDEF, + name=_UNDEF, + sw_version=_UNDEF, hub_device_id=_UNDEF): """Update device attributes.""" old = self.devices[device_id] @@ -104,21 +112,34 @@ class DeviceRegistry: config_entries = old.config_entries - if (config_entry_id is not _UNDEF and - config_entry_id not in old.config_entries): - config_entries = old.config_entries | {config_entry_id} + if (add_config_entry_id is not _UNDEF and + add_config_entry_id not in old.config_entries): + config_entries = old.config_entries | {add_config_entry_id} if (remove_config_entry_id is not _UNDEF and remove_config_entry_id in config_entries): - config_entries = set(config_entries) - config_entries.remove(remove_config_entry_id) + config_entries = config_entries - {remove_config_entry_id} if config_entries is not old.config_entries: changes['config_entries'] = config_entries - if (hub_device_id is not _UNDEF and - hub_device_id != old.hub_device_id): - changes['hub_device_id'] = hub_device_id + for attr_name, value in ( + ('connections', merge_connections), + ('identifiers', merge_identifiers), + ): + old_value = getattr(old, attr_name) + if value is not _UNDEF and value != old_value: + changes[attr_name] = old_value | value + + for attr_name, value in ( + ('manufacturer', manufacturer), + ('model', model), + ('name', name), + ('sw_version', sw_version), + ('hub_device_id', hub_device_id), + ): + if value is not _UNDEF and value != getattr(old, attr_name): + changes[attr_name] = value if not changes: return old diff --git a/tests/components/config/test_device_registry.py b/tests/components/config/test_device_registry.py index f8ea51cfdc8..87eb0fb2d6f 100644 --- a/tests/components/config/test_device_registry.py +++ b/tests/components/config/test_device_registry.py @@ -27,7 +27,6 @@ async def test_list_devices(hass, client, registry): manufacturer='manufacturer', model='model') registry.async_get_or_create( config_entry_id='1234', - connections={}, identifiers={('bridgeid', '1234')}, manufacturer='manufacturer', model='model', via_hub=('bridgeid', '0123')) diff --git a/tests/helpers/test_device_registry.py b/tests/helpers/test_device_registry.py index b251846c491..a87ad3d483a 100644 --- a/tests/helpers/test_device_registry.py +++ b/tests/helpers/test_device_registry.py @@ -17,7 +17,10 @@ async def test_get_or_create_returns_same_entry(registry): config_entry_id='1234', connections={('ethernet', '12:34:56:78:90:AB:CD:EF')}, identifiers={('bridgeid', '0123')}, - manufacturer='manufacturer', model='model') + sw_version='sw-version', + name='name', + manufacturer='manufacturer', + model='model') entry2 = registry.async_get_or_create( config_entry_id='1234', connections={('ethernet', '11:22:33:44:55:66:77:88')}, @@ -25,15 +28,19 @@ async def test_get_or_create_returns_same_entry(registry): manufacturer='manufacturer', model='model') entry3 = registry.async_get_or_create( config_entry_id='1234', - connections={('ethernet', '12:34:56:78:90:AB:CD:EF')}, - identifiers={('bridgeid', '1234')}, - manufacturer='manufacturer', model='model') + connections={('ethernet', '12:34:56:78:90:AB:CD:EF')} + ) assert len(registry.devices) == 1 assert entry.id == entry2.id assert entry.id == entry3.id assert entry.identifiers == {('bridgeid', '0123')} + assert entry3.manufacturer == 'manufacturer' + assert entry3.model == 'model' + assert entry3.name == 'name' + assert entry3.sw_version == 'sw-version' + async def test_requirement_for_identifier_or_connection(registry): """Make sure we do require some descriptor of device.""" From ad79dc673d22fe7086e67fa393c36bd105bb7484 Mon Sep 17 00:00:00 2001 From: emontnemery Date: Thu, 27 Sep 2018 11:48:52 +0200 Subject: [PATCH 085/247] MQTT Light - Do not throw if property is missing from templated MQTT message (#16720) * Do not throw if property is missing * Render template once, add debug prints --- homeassistant/components/light/mqtt.py | 52 ++++++++++++++++++++++---- 1 file changed, 44 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/light/mqtt.py b/homeassistant/components/light/mqtt.py index 10227bdc3b6..8bd50700c53 100644 --- a/homeassistant/components/light/mqtt.py +++ b/homeassistant/components/light/mqtt.py @@ -243,6 +243,10 @@ class MqttLight(MqttAvailability, MqttDiscoveryUpdate, Light): def state_received(topic, payload, qos): """Handle new MQTT messages.""" payload = templates[CONF_STATE](payload) + if not payload: + _LOGGER.debug("Ignoring empty state message from '%s'", topic) + return + if payload == self._payload['on']: self._state = True elif payload == self._payload['off']: @@ -259,7 +263,13 @@ class MqttLight(MqttAvailability, MqttDiscoveryUpdate, Light): @callback def brightness_received(topic, payload, qos): """Handle new MQTT messages for the brightness.""" - device_value = float(templates[CONF_BRIGHTNESS](payload)) + payload = templates[CONF_BRIGHTNESS](payload) + if not payload: + _LOGGER.debug("Ignoring empty brightness message from '%s'", + topic) + return + + device_value = float(payload) percent_bright = device_value / self._brightness_scale self._brightness = int(percent_bright * 255) self.async_schedule_update_ha_state() @@ -280,8 +290,12 @@ class MqttLight(MqttAvailability, MqttDiscoveryUpdate, Light): @callback def rgb_received(topic, payload, qos): """Handle new MQTT messages for RGB.""" - rgb = [int(val) for val in - templates[CONF_RGB](payload).split(',')] + payload = templates[CONF_RGB](payload) + if not payload: + _LOGGER.debug("Ignoring empty rgb message from '%s'", topic) + return + + rgb = [int(val) for val in payload.split(',')] self._hs = color_util.color_RGB_to_hs(*rgb) self.async_schedule_update_ha_state() @@ -299,7 +313,13 @@ class MqttLight(MqttAvailability, MqttDiscoveryUpdate, Light): @callback def color_temp_received(topic, payload, qos): """Handle new MQTT messages for color temperature.""" - self._color_temp = int(templates[CONF_COLOR_TEMP](payload)) + payload = templates[CONF_COLOR_TEMP](payload) + if not payload: + _LOGGER.debug("Ignoring empty color temp message from '%s'", + topic) + return + + self._color_temp = int(payload) self.async_schedule_update_ha_state() if self._topic[CONF_COLOR_TEMP_STATE_TOPIC] is not None: @@ -318,7 +338,12 @@ class MqttLight(MqttAvailability, MqttDiscoveryUpdate, Light): @callback def effect_received(topic, payload, qos): """Handle new MQTT messages for effect.""" - self._effect = templates[CONF_EFFECT](payload) + payload = templates[CONF_EFFECT](payload) + if not payload: + _LOGGER.debug("Ignoring empty effect message from '%s'", topic) + return + + self._effect = payload self.async_schedule_update_ha_state() if self._topic[CONF_EFFECT_STATE_TOPIC] is not None: @@ -337,7 +362,13 @@ class MqttLight(MqttAvailability, MqttDiscoveryUpdate, Light): @callback def white_value_received(topic, payload, qos): """Handle new MQTT messages for white value.""" - device_value = float(templates[CONF_WHITE_VALUE](payload)) + payload = templates[CONF_WHITE_VALUE](payload) + if not payload: + _LOGGER.debug("Ignoring empty white value message from '%s'", + topic) + return + + device_value = float(payload) percent_white = device_value / self._white_value_scale self._white_value = int(percent_white * 255) self.async_schedule_update_ha_state() @@ -358,8 +389,13 @@ class MqttLight(MqttAvailability, MqttDiscoveryUpdate, Light): @callback def xy_received(topic, payload, qos): """Handle new MQTT messages for color.""" - xy_color = [float(val) for val in - templates[CONF_XY](payload).split(',')] + payload = templates[CONF_XY](payload) + if not payload: + _LOGGER.debug("Ignoring empty xy-color message from '%s'", + topic) + return + + xy_color = [float(val) for val in payload.split(',')] self._hs = color_util.color_xy_to_hs(*xy_color) self.async_schedule_update_ha_state() From 2cc626309285dba1ad21f0ffd65017ccc2dfa7d4 Mon Sep 17 00:00:00 2001 From: Blake Blackshear Date: Thu, 27 Sep 2018 05:34:42 -0500 Subject: [PATCH 086/247] Add new services for set/refresh Z-Wave device values (#16638) * Add services for getting and setting indicator values for Z-Wave * Add service to manually refresh Z-Wave node value by value_id * Remove refresh_indicator service * Coerce to int * Add generic set_node_value service * Remove set_indicator service --- homeassistant/components/zwave/__init__.py | 34 ++++++++++++++ homeassistant/components/zwave/const.py | 2 + .../components/zwave/discovery_schemas.py | 3 +- homeassistant/components/zwave/services.yaml | 18 +++++++ tests/components/zwave/test_init.py | 47 +++++++++++++++++++ 5 files changed, 103 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/zwave/__init__.py b/homeassistant/components/zwave/__init__.py index 4cb2f6b0f7b..8de8c713f04 100644 --- a/homeassistant/components/zwave/__init__.py +++ b/homeassistant/components/zwave/__init__.py @@ -84,6 +84,17 @@ SET_CONFIG_PARAMETER_SCHEMA = vol.Schema({ vol.Optional(const.ATTR_CONFIG_SIZE, default=2): vol.Coerce(int) }) +SET_NODE_VALUE_SCHEMA = vol.Schema({ + vol.Required(const.ATTR_NODE_ID): vol.Coerce(int), + vol.Required(const.ATTR_VALUE_ID): vol.Coerce(int), + vol.Required(const.ATTR_CONFIG_VALUE): vol.Coerce(int) +}) + +REFRESH_NODE_VALUE_SCHEMA = vol.Schema({ + vol.Required(const.ATTR_NODE_ID): vol.Coerce(int), + vol.Required(const.ATTR_VALUE_ID): vol.Coerce(int) +}) + SET_POLL_INTENSITY_SCHEMA = vol.Schema({ vol.Required(const.ATTR_NODE_ID): vol.Coerce(int), vol.Required(const.ATTR_VALUE_ID): vol.Coerce(int), @@ -492,6 +503,23 @@ async def async_setup(hass, config): "with selection %s", param, node_id, selection) + def refresh_node_value(service): + """Refresh the specified value from a node.""" + node_id = service.data.get(const.ATTR_NODE_ID) + value_id = service.data.get(const.ATTR_VALUE_ID) + node = network.nodes[node_id] + node.values[value_id].refresh() + _LOGGER.info("Node %s value %s refreshed", node_id, value_id) + + def set_node_value(service): + """Set the specified value on a node.""" + node_id = service.data.get(const.ATTR_NODE_ID) + value_id = service.data.get(const.ATTR_VALUE_ID) + value = service.data.get(const.ATTR_CONFIG_VALUE) + node = network.nodes[node_id] + node.values[value_id].data = value + _LOGGER.info("Node %s value %s set to %s", node_id, value_id, value) + def print_config_parameter(service): """Print a config parameter from a node.""" node_id = service.data.get(const.ATTR_NODE_ID) @@ -662,6 +690,12 @@ async def async_setup(hass, config): hass.services.register(DOMAIN, const.SERVICE_SET_CONFIG_PARAMETER, set_config_parameter, schema=SET_CONFIG_PARAMETER_SCHEMA) + hass.services.register(DOMAIN, const.SERVICE_SET_NODE_VALUE, + set_node_value, + schema=SET_NODE_VALUE_SCHEMA) + hass.services.register(DOMAIN, const.SERVICE_REFRESH_NODE_VALUE, + refresh_node_value, + schema=REFRESH_NODE_VALUE_SCHEMA) hass.services.register(DOMAIN, const.SERVICE_PRINT_CONFIG_PARAMETER, print_config_parameter, schema=PRINT_CONFIG_PARAMETER_SCHEMA) diff --git a/homeassistant/components/zwave/const.py b/homeassistant/components/zwave/const.py index 0228e64cf6e..775da8fbc51 100644 --- a/homeassistant/components/zwave/const.py +++ b/homeassistant/components/zwave/const.py @@ -39,6 +39,8 @@ SERVICE_SOFT_RESET = "soft_reset" SERVICE_TEST_NODE = "test_node" SERVICE_TEST_NETWORK = "test_network" SERVICE_SET_CONFIG_PARAMETER = "set_config_parameter" +SERVICE_SET_NODE_VALUE = "set_node_value" +SERVICE_REFRESH_NODE_VALUE = "refresh_node_value" SERVICE_PRINT_CONFIG_PARAMETER = "print_config_parameter" SERVICE_PRINT_NODE = "print_node" SERVICE_REMOVE_FAILED_NODE = "remove_failed_node" diff --git a/homeassistant/components/zwave/discovery_schemas.py b/homeassistant/components/zwave/discovery_schemas.py index 2a4e42ab92c..56d63d658a9 100644 --- a/homeassistant/components/zwave/discovery_schemas.py +++ b/homeassistant/components/zwave/discovery_schemas.py @@ -211,7 +211,8 @@ DISCOVERY_SCHEMAS = [ const.COMMAND_CLASS_SENSOR_MULTILEVEL, const.COMMAND_CLASS_METER, const.COMMAND_CLASS_ALARM, - const.COMMAND_CLASS_SENSOR_ALARM], + const.COMMAND_CLASS_SENSOR_ALARM, + const.COMMAND_CLASS_INDICATOR], const.DISC_GENRE: const.GENRE_USER, }})}, {const.DISC_COMPONENT: 'switch', diff --git a/homeassistant/components/zwave/services.yaml b/homeassistant/components/zwave/services.yaml index 1762c33237d..583d76a1955 100644 --- a/homeassistant/components/zwave/services.yaml +++ b/homeassistant/components/zwave/services.yaml @@ -69,6 +69,24 @@ set_config_parameter: size: description: (Optional) Set the size of the parameter value. Only needed if no parameters are available. +set_node_value: + description: Set the value for a given value_id on a Z-Wave device. + fields: + node_id: + description: Node id of the device to set the value on (integer). + value_id: + description: Value id of the value to set (integer). + value: + description: Value to set (integer). + +refresh_node_value: + description: Refresh the value for a given value_id on a Z-Wave device. + fields: + node_id: + description: Node id of the device to refresh value from (integer). + value_id: + description: Value id of the value to refresh. + set_poll_intensity: description: Set the polling interval to a nodes value fields: diff --git a/tests/components/zwave/test_init.py b/tests/components/zwave/test_init.py index bc96c90d50a..2b3019b2f8d 100644 --- a/tests/components/zwave/test_init.py +++ b/tests/components/zwave/test_init.py @@ -1361,6 +1361,53 @@ class TestZWaveServices(unittest.TestCase): assert node.refresh_info.called assert len(node.refresh_info.mock_calls) == 1 + def test_set_node_value(self): + """Test zwave set_node_value service.""" + value = MockValue( + index=12, + command_class=const.COMMAND_CLASS_INDICATOR, + data=4 + ) + node = MockNode(node_id=14, + command_classes=[const.COMMAND_CLASS_INDICATOR]) + node.values = {12: value} + node.get_values.return_value = node.values + self.zwave_network.nodes = {14: node} + + self.hass.services.call('zwave', 'set_node_value', { + const.ATTR_NODE_ID: 14, + const.ATTR_VALUE_ID: 12, + const.ATTR_CONFIG_VALUE: 2, + }) + self.hass.block_till_done() + + assert self.zwave_network.nodes[14].values[12].data == 2 + + def test_refresh_node_value(self): + """Test zwave refresh_node_value service.""" + node = MockNode(node_id=14, + command_classes=[const.COMMAND_CLASS_INDICATOR], + network=self.zwave_network) + value = MockValue( + node=node, + index=12, + command_class=const.COMMAND_CLASS_INDICATOR, + data=2 + ) + value.refresh = MagicMock() + + node.values = {12: value} + node.get_values.return_value = node.values + self.zwave_network.nodes = {14: node} + + self.hass.services.call('zwave', 'refresh_node_value', { + const.ATTR_NODE_ID: 14, + const.ATTR_VALUE_ID: 12 + }) + self.hass.block_till_done() + + assert value.refresh.called + def test_heal_node(self): """Test zwave heal_node service.""" node = MockNode(node_id=19) From 8d65230a36aad62952d857c8ca93e7d5c0fdc02a Mon Sep 17 00:00:00 2001 From: zxdavb Date: Thu, 27 Sep 2018 12:29:44 +0100 Subject: [PATCH 087/247] Add (EU-based) Honeywell evohome CH/DHW controller (#16427) * Add support for Honeywell evohome CH/DHW systems More flake8 corrections Passes Flake8 tests Almost passed flake8.pylint! Passed all tox tests Now it needs serious testing! Almost ready to submit BUGFIX: DHW state now functional More improvements to available() Solved the DHW temp units problem! Last minute bug squash to improve dicts merge Trying to rebase fixing more rbase errors revert to creating HTTP_error_code internally for now ready to submit PR Added support for Honeywell evohome CH/DHW systems * Updated requirements_test_all.txt * Fix: D401 First line should be in imperative mood * Remove _LOGGER.info (replace with _LOGGER.debug) * raise PlatformNotReady when RequestException during setup() * Revert some LOGGER.debug to LOGGER.warning * Improved logging levels, and removed some unwanted comments * Improvments to logging - additional self._status info * BUGFIX: DHW wrongly showing available = False (and some other tweaks) * Fix trailing whitespace * Remove state_attributes override and API_VER code * Removed heating zones, DHW and heuristics to reduce # lines of code * Removed some more lines of code * Removed unused configuration parameters * Remove some more un-needed lines * Removed more (uneeded) lines of code & fixed two minor typos * Improvements to debug logging of available() = False * Improvements to code, and code clean-up * Corrected a minor typo * A small tidy up * reduces precision of emulated temps floats to 0.1 * Some code improvements as suggested by JeardM * Rewrite of exception handler * Removed another unwanted logging in properties * Remove async_ version of various methods * moved available heuristics to update() * Cleanup of code, and re-work linter hints * fixed a minor documentation typo * scan_interval is now no longer a configurable option * Change from Master/Slave to Parent/Child * Removed the last of the slaves * Removed the last of the masters * Move -PARALLEL_UPDATES to .\climate\evohome.py' * main code moved to climate/evohome.py * merge EvoEntity into EvoController class * remove should_poll (for now) * woops! left a hint in * removed icon * only log a WARNING the first time available = False * cleanup dodgy exception handling * Tidy up exception handling * Many changes as suggested by @MartinHjelmare, thanks * remove hass from init, part 1 * use async_added_to_hass instead of dispatcher_connect * remove hass from init, part 2 (done) * add 1-2 arrays, and tidied up some comments * from dispatcher to async_added_to_hass * cleaned up some logging, and removed others * Many changes as request by @MartinHjelmare * Homage to the lint * Changed to the HA of doing operating_mode * Now using update_before_add=True * reduced logging further still... * fixed minor lint * fix a small logic error * Add device_state_attributes to track actual operating mode * Clean up doc quotes caused by previous changes * Woops! removed some debug lines that shoudln't have stayed in * Add a complete set of device_state_attributes * Cleanup some constants * Remove more legacy code * domain_data to evo_data & this else should be a finally * minor change for readability * Minor change for readability #2 * removed unused code * small tidy up - no code changes * fix minor lint * Correct URLs & descriptions in docstring * whoops - fixed a typo in docstrings * remove an unused line of cide & a small tidy-up --- .coveragerc | 3 + homeassistant/components/climate/evohome.py | 371 ++++++++++++++++++ homeassistant/components/climate/honeywell.py | 2 +- homeassistant/components/evohome.py | 145 +++++++ homeassistant/const.py | 2 + requirements_all.txt | 3 +- requirements_test_all.txt | 3 +- 7 files changed, 526 insertions(+), 3 deletions(-) create mode 100644 homeassistant/components/climate/evohome.py create mode 100644 homeassistant/components/evohome.py diff --git a/.coveragerc b/.coveragerc index a935564791a..5e522016096 100644 --- a/.coveragerc +++ b/.coveragerc @@ -105,6 +105,9 @@ omit = homeassistant/components/envisalink.py homeassistant/components/*/envisalink.py + homeassistant/components/evohome.py + homeassistant/components/*/evohome.py + homeassistant/components/fritzbox.py homeassistant/components/switch/fritzbox.py diff --git a/homeassistant/components/climate/evohome.py b/homeassistant/components/climate/evohome.py new file mode 100644 index 00000000000..f0631228fd8 --- /dev/null +++ b/homeassistant/components/climate/evohome.py @@ -0,0 +1,371 @@ +"""Support for Honeywell evohome (EMEA/EU-based systems only). + +Support for a temperature control system (TCS, controller) with 0+ heating +zones (e.g. TRVs, relays) and, optionally, a DHW controller. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/climate.evohome/ +""" + +from datetime import datetime, timedelta +import logging + +from requests.exceptions import HTTPError + +from homeassistant.components.climate import ( + ClimateDevice, + STATE_AUTO, + STATE_ECO, + STATE_OFF, + SUPPORT_OPERATION_MODE, + SUPPORT_AWAY_MODE, +) +from homeassistant.components.evohome import ( + CONF_LOCATION_IDX, + DATA_EVOHOME, + MAX_TEMP, + MIN_TEMP, + SCAN_INTERVAL_MAX +) +from homeassistant.const import ( + CONF_SCAN_INTERVAL, + PRECISION_TENTHS, + TEMP_CELSIUS, + HTTP_TOO_MANY_REQUESTS, +) +_LOGGER = logging.getLogger(__name__) + +# these are for the controller's opmode/state and the zone's state +EVO_RESET = 'AutoWithReset' +EVO_AUTO = 'Auto' +EVO_AUTOECO = 'AutoWithEco' +EVO_AWAY = 'Away' +EVO_DAYOFF = 'DayOff' +EVO_CUSTOM = 'Custom' +EVO_HEATOFF = 'HeatingOff' + +EVO_STATE_TO_HA = { + EVO_RESET: STATE_AUTO, + EVO_AUTO: STATE_AUTO, + EVO_AUTOECO: STATE_ECO, + EVO_AWAY: STATE_AUTO, + EVO_DAYOFF: STATE_AUTO, + EVO_CUSTOM: STATE_AUTO, + EVO_HEATOFF: STATE_OFF +} + +HA_STATE_TO_EVO = { + STATE_AUTO: EVO_AUTO, + STATE_ECO: EVO_AUTOECO, + STATE_OFF: EVO_HEATOFF +} + +HA_OP_LIST = list(HA_STATE_TO_EVO) + +# these are used to help prevent E501 (line too long) violations +GWS = 'gateways' +TCS = 'temperatureControlSystems' + +# debug codes - these happen occasionally, but the cause is unknown +EVO_DEBUG_NO_RECENT_UPDATES = '0x01' +EVO_DEBUG_NO_STATUS = '0x02' + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Create a Honeywell (EMEA/EU) evohome CH/DHW system. + + An evohome system consists of: a controller, with 0-12 heating zones (e.g. + TRVs, relays) and, optionally, a DHW controller (a HW boiler). + + Here, we add the controller only. + """ + evo_data = hass.data[DATA_EVOHOME] + + client = evo_data['client'] + loc_idx = evo_data['params'][CONF_LOCATION_IDX] + + # evohomeclient has no defined way of accessing non-default location other + # than using a protected member, such as below + tcs_obj_ref = client.locations[loc_idx]._gateways[0]._control_systems[0] # noqa E501; pylint: disable=protected-access + + _LOGGER.debug( + "setup_platform(): Found Controller: id: %s [%s], type: %s", + tcs_obj_ref.systemId, + tcs_obj_ref.location.name, + tcs_obj_ref.modelType + ) + parent = EvoController(evo_data, client, tcs_obj_ref) + add_entities([parent], update_before_add=True) + + +class EvoController(ClimateDevice): + """Base for a Honeywell evohome hub/Controller device. + + The Controller (aka TCS, temperature control system) is the parent of all + the child (CH/DHW) devices. + """ + + def __init__(self, evo_data, client, obj_ref): + """Initialize the evohome entity. + + Most read-only properties are set here. So are pseudo read-only, + for example name (which _could_ change between update()s). + """ + self.client = client + self._obj = obj_ref + + self._id = obj_ref.systemId + self._name = evo_data['config']['locationInfo']['name'] + + self._config = evo_data['config'][GWS][0][TCS][0] + self._params = evo_data['params'] + self._timers = evo_data['timers'] + + self._timers['statusUpdated'] = datetime.min + self._status = {} + + self._available = False # should become True after first update() + + def _handle_requests_exceptions(self, err): + # evohomeclient v2 api (>=0.2.7) exposes requests exceptions, incl.: + # - HTTP_BAD_REQUEST, is usually Bad user credentials + # - HTTP_TOO_MANY_REQUESTS, is api usuage limit exceeded + # - HTTP_SERVICE_UNAVAILABLE, is often Vendor's fault + + if err.response.status_code == HTTP_TOO_MANY_REQUESTS: + # execute a back off: pause, and reduce rate + old_scan_interval = self._params[CONF_SCAN_INTERVAL] + new_scan_interval = min(old_scan_interval * 2, SCAN_INTERVAL_MAX) + self._params[CONF_SCAN_INTERVAL] = new_scan_interval + + _LOGGER.warning( + "API rate limit has been exceeded: increasing '%s' from %s to " + "%s seconds, and suspending polling for %s seconds.", + CONF_SCAN_INTERVAL, + old_scan_interval, + new_scan_interval, + new_scan_interval * 3 + ) + + self._timers['statusUpdated'] = datetime.now() + \ + timedelta(seconds=new_scan_interval * 3) + + else: + raise err + + @property + def name(self): + """Return the name to use in the frontend UI.""" + return self._name + + @property + def available(self): + """Return True if the device is available. + + All evohome entities are initially unavailable. Once HA has started, + state data is then retrieved by the Controller, and then the children + will get a state (e.g. operating_mode, current_temperature). + + However, evohome entities can become unavailable for other reasons. + """ + return self._available + + @property + def supported_features(self): + """Get the list of supported features of the Controller.""" + return SUPPORT_OPERATION_MODE | SUPPORT_AWAY_MODE + + @property + def device_state_attributes(self): + """Return the device state attributes of the controller. + + This is operating mode state data that is not available otherwise, due + to the restrictions placed upon ClimateDevice properties, etc by HA. + """ + data = {} + data['systemMode'] = self._status['systemModeStatus']['mode'] + data['isPermanent'] = self._status['systemModeStatus']['isPermanent'] + if 'timeUntil' in self._status['systemModeStatus']: + data['timeUntil'] = self._status['systemModeStatus']['timeUntil'] + data['activeFaults'] = self._status['activeFaults'] + return data + + @property + def operation_list(self): + """Return the list of available operations.""" + return HA_OP_LIST + + @property + def current_operation(self): + """Return the operation mode of the evohome entity.""" + return EVO_STATE_TO_HA.get(self._status['systemModeStatus']['mode']) + + @property + def target_temperature(self): + """Return the average target temperature of the Heating/DHW zones.""" + temps = [zone['setpointStatus']['targetHeatTemperature'] + for zone in self._status['zones']] + + avg_temp = round(sum(temps) / len(temps), 1) if temps else None + return avg_temp + + @property + def current_temperature(self): + """Return the average current temperature of the Heating/DHW zones.""" + tmp_list = [x for x in self._status['zones'] + if x['temperatureStatus']['isAvailable'] is True] + temps = [zone['temperatureStatus']['temperature'] for zone in tmp_list] + + avg_temp = round(sum(temps) / len(temps), 1) if temps else None + return avg_temp + + @property + def temperature_unit(self): + """Return the temperature unit to use in the frontend UI.""" + return TEMP_CELSIUS + + @property + def precision(self): + """Return the temperature precision to use in the frontend UI.""" + return PRECISION_TENTHS + + @property + def min_temp(self): + """Return the minimum target temp (setpoint) of a evohome entity.""" + return MIN_TEMP + + @property + def max_temp(self): + """Return the maximum target temp (setpoint) of a evohome entity.""" + return MAX_TEMP + + @property + def is_on(self): + """Return true as evohome controllers are always on. + + Operating modes can include 'HeatingOff', but (for example) DHW would + remain on. + """ + return True + + @property + def is_away_mode_on(self): + """Return true if away mode is on.""" + return self._status['systemModeStatus']['mode'] == EVO_AWAY + + def turn_away_mode_on(self): + """Turn away mode on.""" + self._set_operation_mode(EVO_AWAY) + + def turn_away_mode_off(self): + """Turn away mode off.""" + self._set_operation_mode(EVO_AUTO) + + def _set_operation_mode(self, operation_mode): + # Set new target operation mode for the TCS. + _LOGGER.debug( + "_set_operation_mode(): API call [1 request(s)]: " + "tcs._set_status(%s)...", + operation_mode + ) + try: + self._obj._set_status(operation_mode) # noqa: E501; pylint: disable=protected-access + except HTTPError as err: + self._handle_requests_exceptions(err) + + def set_operation_mode(self, operation_mode): + """Set new target operation mode for the TCS. + + Currently limited to 'Auto', 'AutoWithEco' & 'HeatingOff'. If 'Away' + mode is needed, it can be enabled via turn_away_mode_on method. + """ + self._set_operation_mode(HA_STATE_TO_EVO.get(operation_mode)) + + def _update_state_data(self, evo_data): + client = evo_data['client'] + loc_idx = evo_data['params'][CONF_LOCATION_IDX] + + _LOGGER.debug( + "_update_state_data(): API call [1 request(s)]: " + "client.locations[loc_idx].status()..." + ) + + try: + evo_data['status'].update( + client.locations[loc_idx].status()[GWS][0][TCS][0]) + except HTTPError as err: # check if we've exceeded the api rate limit + self._handle_requests_exceptions(err) + else: + evo_data['timers']['statusUpdated'] = datetime.now() + + _LOGGER.debug( + "_update_state_data(): evo_data['status'] = %s", + evo_data['status'] + ) + + def update(self): + """Get the latest state data of the installation. + + This includes state data for the Controller and its child devices, such + as the operating_mode of the Controller and the current_temperature + of its children. + + This is not asyncio-friendly due to the underlying client api. + """ + evo_data = self.hass.data[DATA_EVOHOME] + + timeout = datetime.now() + timedelta(seconds=55) + expired = timeout > self._timers['statusUpdated'] + \ + timedelta(seconds=evo_data['params'][CONF_SCAN_INTERVAL]) + + if not expired: + return + + was_available = self._available or \ + self._timers['statusUpdated'] == datetime.min + + self._update_state_data(evo_data) + self._status = evo_data['status'] + + if _LOGGER.isEnabledFor(logging.DEBUG): + tmp_dict = dict(self._status) + if 'zones' in tmp_dict: + tmp_dict['zones'] = '...' + if 'dhw' in tmp_dict: + tmp_dict['dhw'] = '...' + + _LOGGER.debug( + "update(%s), self._status = %s", + self._id + " [" + self._name + "]", + tmp_dict + ) + + no_recent_updates = self._timers['statusUpdated'] < datetime.now() - \ + timedelta(seconds=self._params[CONF_SCAN_INTERVAL] * 3.1) + + if no_recent_updates: + self._available = False + debug_code = EVO_DEBUG_NO_RECENT_UPDATES + + elif not self._status: + # unavailable because no status (but how? other than at startup?) + self._available = False + debug_code = EVO_DEBUG_NO_STATUS + + else: + self._available = True + + if not self._available and was_available: + # only warn if available went from True to False + _LOGGER.warning( + "The entity, %s, has become unavailable, debug code is: %s", + self._id + " [" + self._name + "]", + debug_code + ) + + elif self._available and not was_available: + # this isn't the first re-available (e.g. _after_ STARTUP) + _LOGGER.debug( + "The entity, %s, has become available", + self._id + " [" + self._name + "]" + ) diff --git a/homeassistant/components/climate/honeywell.py b/homeassistant/components/climate/honeywell.py index 6d54695fa7a..c445a495073 100644 --- a/homeassistant/components/climate/honeywell.py +++ b/homeassistant/components/climate/honeywell.py @@ -20,7 +20,7 @@ from homeassistant.const import ( CONF_PASSWORD, CONF_USERNAME, TEMP_CELSIUS, TEMP_FAHRENHEIT, ATTR_TEMPERATURE, CONF_REGION) -REQUIREMENTS = ['evohomeclient==0.2.5', 'somecomfort==0.5.2'] +REQUIREMENTS = ['evohomeclient==0.2.7', 'somecomfort==0.5.2'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/evohome.py b/homeassistant/components/evohome.py new file mode 100644 index 00000000000..ceeec407b05 --- /dev/null +++ b/homeassistant/components/evohome.py @@ -0,0 +1,145 @@ +"""Support for Honeywell evohome (EMEA/EU-based systems only). + +Support for a temperature control system (TCS, controller) with 0+ heating +zones (e.g. TRVs, relays) and, optionally, a DHW controller. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/evohome/ +""" + +# Glossary: +# TCS - temperature control system (a.k.a. Controller, Parent), which can +# have up to 13 Children: +# 0-12 Heating zones (a.k.a. Zone), and +# 0-1 DHW controller, (a.k.a. Boiler) + +import logging + +from requests.exceptions import HTTPError +import voluptuous as vol + +from homeassistant.const import ( + CONF_USERNAME, + CONF_PASSWORD, + CONF_SCAN_INTERVAL, + HTTP_BAD_REQUEST +) + +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.discovery import load_platform + +REQUIREMENTS = ['evohomeclient==0.2.7'] +# If ever > 0.2.7, re-check the work-around wrapper is still required when +# instantiating the client, below. + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = 'evohome' +DATA_EVOHOME = 'data_' + DOMAIN + +CONF_LOCATION_IDX = 'location_idx' +MAX_TEMP = 28 +MIN_TEMP = 5 +SCAN_INTERVAL_DEFAULT = 180 +SCAN_INTERVAL_MAX = 300 + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_LOCATION_IDX, default=0): cv.positive_int, + }), +}, extra=vol.ALLOW_EXTRA) + +# These are used to help prevent E501 (line too long) violations. +GWS = 'gateways' +TCS = 'temperatureControlSystems' + + +def setup(hass, config): + """Create a Honeywell (EMEA/EU) evohome CH/DHW system. + + One controller with 0+ heating zones (e.g. TRVs, relays) and, optionally, a + DHW controller. Does not work for US-based systems. + """ + evo_data = hass.data[DATA_EVOHOME] = {} + evo_data['timers'] = {} + + evo_data['params'] = dict(config[DOMAIN]) + evo_data['params'][CONF_SCAN_INTERVAL] = SCAN_INTERVAL_DEFAULT + + from evohomeclient2 import EvohomeClient + + _LOGGER.debug("setup(): API call [4 request(s)]: client.__init__()...") + + try: + # There's a bug in evohomeclient2 v0.2.7: the client.__init__() sets + # the root loglevel when EvohomeClient(debug=?), so remember it now... + log_level = logging.getLogger().getEffectiveLevel() + + client = EvohomeClient( + evo_data['params'][CONF_USERNAME], + evo_data['params'][CONF_PASSWORD], + debug=False + ) + # ...then restore it to what it was before instantiating the client + logging.getLogger().setLevel(log_level) + + except HTTPError as err: + if err.response.status_code == HTTP_BAD_REQUEST: + _LOGGER.error( + "Failed to establish a connection with evohome web servers, " + "Check your username (%s), and password are correct." + "Unable to continue. Resolve any errors and restart HA.", + evo_data['params'][CONF_USERNAME] + ) + return False # unable to continue + + raise # we dont handle any other HTTPErrors + + finally: # Redact username, password as no longer needed. + evo_data['params'][CONF_USERNAME] = 'REDACTED' + evo_data['params'][CONF_PASSWORD] = 'REDACTED' + + evo_data['client'] = client + + # Redact any installation data we'll never need. + if client.installation_info[0]['locationInfo']['locationId'] != 'REDACTED': + for loc in client.installation_info: + loc['locationInfo']['streetAddress'] = 'REDACTED' + loc['locationInfo']['city'] = 'REDACTED' + loc['locationInfo']['locationOwner'] = 'REDACTED' + loc[GWS][0]['gatewayInfo'] = 'REDACTED' + + # Pull down the installation configuration. + loc_idx = evo_data['params'][CONF_LOCATION_IDX] + + try: + evo_data['config'] = client.installation_info[loc_idx] + + except IndexError: + _LOGGER.warning( + "setup(): Parameter '%s' = %s , is outside its range (0-%s)", + CONF_LOCATION_IDX, + loc_idx, + len(client.installation_info) - 1 + ) + + return False # unable to continue + + evo_data['status'] = {} + + if _LOGGER.isEnabledFor(logging.DEBUG): + tmp_loc = dict(evo_data['config']) + tmp_loc['locationInfo']['postcode'] = 'REDACTED' + tmp_tcs = tmp_loc[GWS][0][TCS][0] + if 'zones' in tmp_tcs: + tmp_tcs['zones'] = '...' + if 'dhw' in tmp_tcs: + tmp_tcs['dhw'] = '...' + + _LOGGER.debug("setup(), location = %s", tmp_loc) + + load_platform(hass, 'climate', DOMAIN) + + return True diff --git a/homeassistant/const.py b/homeassistant/const.py index 0bcfcd9d4ad..1d3adf7ee45 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -410,7 +410,9 @@ HTTP_UNAUTHORIZED = 401 HTTP_NOT_FOUND = 404 HTTP_METHOD_NOT_ALLOWED = 405 HTTP_UNPROCESSABLE_ENTITY = 422 +HTTP_TOO_MANY_REQUESTS = 429 HTTP_INTERNAL_SERVER_ERROR = 500 +HTTP_SERVICE_UNAVAILABLE = 503 HTTP_BASIC_AUTHENTICATION = 'basic' HTTP_DIGEST_AUTHENTICATION = 'digest' diff --git a/requirements_all.txt b/requirements_all.txt index a954b2cf94f..6efd9a21d7e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -338,8 +338,9 @@ eternalegypt==0.0.5 # homeassistant.components.keyboard_remote # evdev==0.6.1 +# homeassistant.components.evohome # homeassistant.components.climate.honeywell -evohomeclient==0.2.5 +evohomeclient==0.2.7 # homeassistant.components.image_processing.dlib_face_detect # homeassistant.components.image_processing.dlib_face_identify diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f33b37e01d1..1f7d58ef6aa 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -52,8 +52,9 @@ dsmr_parser==0.11 # homeassistant.components.sensor.season ephem==3.7.6.0 +# homeassistant.components.evohome # homeassistant.components.climate.honeywell -evohomeclient==0.2.5 +evohomeclient==0.2.7 # homeassistant.components.feedreader # homeassistant.components.sensor.geo_rss_events From d1ad2cc22536df7ff65c578f054685a608077dc3 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Thu, 27 Sep 2018 16:07:56 +0200 Subject: [PATCH 088/247] Make MQTT platforms config entries (#16904) * Make MQTT platforms config entries * Fix tests * Address Comment * Rework tests * Undo style auto-reformat style changes --- homeassistant/components/mqtt/discovery.py | 27 ++++++++++-- homeassistant/components/sensor/mqtt.py | 22 +++++++++- tests/components/mqtt/test_discovery.py | 49 +++++++++++++++++----- tests/components/sensor/test_mqtt.py | 6 ++- 4 files changed, 87 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py index f42c1ed58e9..d5ed5e25b47 100644 --- a/homeassistant/components/mqtt/discovery.py +++ b/homeassistant/components/mqtt/discovery.py @@ -13,6 +13,7 @@ from homeassistant.components.mqtt import CONF_STATE_TOPIC, ATTR_DISCOVERY_HASH from homeassistant.const import CONF_PLATFORM from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.typing import HomeAssistantType _LOGGER = logging.getLogger(__name__) @@ -38,11 +39,18 @@ ALLOWED_PLATFORMS = { 'alarm_control_panel': ['mqtt'], } +CONFIG_ENTRY_PLATFORMS = { + 'sensor': ['mqtt'], +} + ALREADY_DISCOVERED = 'mqtt_discovered_components' +CONFIG_ENTRY_IS_SETUP = 'mqtt_config_entry_is_setup' MQTT_DISCOVERY_UPDATED = 'mqtt_discovery_updated_{}' +MQTT_DISCOVERY_NEW = 'mqtt_discovery_new_{}_{}' -async def async_start(hass, discovery_topic, hass_config): +async def async_start(hass: HomeAssistantType, discovery_topic, hass_config, + config_entry=None) -> bool: """Initialize of MQTT Discovery.""" async def async_device_message_received(topic, payload, qos): """Process the received message.""" @@ -98,8 +106,21 @@ async def async_start(hass, discovery_topic, hass_config): _LOGGER.info("Found new component: %s %s", component, discovery_id) - await async_load_platform( - hass, component, platform, payload, hass_config) + if platform not in CONFIG_ENTRY_PLATFORMS.get(component, []): + await async_load_platform( + hass, component, platform, payload, hass_config) + return + + config_entries_key = '{}.{}'.format(component, platform) + if config_entries_key not in hass.data[CONFIG_ENTRY_IS_SETUP]: + await hass.config_entries.async_forward_entry_setup( + config_entry, component) + hass.data[CONFIG_ENTRY_IS_SETUP].add(config_entries_key) + + async_dispatcher_send(hass, MQTT_DISCOVERY_NEW.format( + component, platform), payload) + + hass.data[CONFIG_ENTRY_IS_SETUP] = set() await mqtt.async_subscribe( hass, discovery_topic + '/#', async_device_message_received, 0) diff --git a/homeassistant/components/sensor/mqtt.py b/homeassistant/components/sensor/mqtt.py index 56e4055ea87..2d15fca755b 100644 --- a/homeassistant/components/sensor/mqtt.py +++ b/homeassistant/components/sensor/mqtt.py @@ -12,10 +12,12 @@ from typing import Optional 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_STATE_TOPIC, CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, MqttAvailability, MqttDiscoveryUpdate) +from homeassistant.components.mqtt.discovery import 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, @@ -24,6 +26,7 @@ from homeassistant.helpers.entity import Entity from homeassistant.components import mqtt import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import HomeAssistantType, ConfigType +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.util import dt as dt_util @@ -53,7 +56,24 @@ PLATFORM_SCHEMA = mqtt.MQTT_RO_PLATFORM_SCHEMA.extend({ async def async_setup_platform(hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None): - """Set up MQTT Sensor.""" + """Set up MQTT sensors through configuration.yaml.""" + await _async_setup_platform(hass, config, async_add_entities, + discovery_info) + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up MQTT sensors dynamically through MQTT discovery.""" + async def async_discover_sensor(config): + """Discover and add a discovered MQTT sensor.""" + await _async_setup_platform(hass, {}, async_add_entities, config) + + async_dispatcher_connect(hass, + MQTT_DISCOVERY_NEW.format(sensor.DOMAIN, 'mqtt'), + async_discover_sensor) + + +async def _async_setup_platform(hass: HomeAssistantType, config: ConfigType, + async_add_entities, discovery_info=None): if discovery_info is not None: config = PLATFORM_SCHEMA(discovery_info) diff --git a/tests/components/mqtt/test_discovery.py b/tests/components/mqtt/test_discovery.py index 9e0ef14a3fa..36b022de7a6 100644 --- a/tests/components/mqtt/test_discovery.py +++ b/tests/components/mqtt/test_discovery.py @@ -2,18 +2,23 @@ import asyncio from unittest.mock import patch +from homeassistant.components import mqtt from homeassistant.components.mqtt.discovery import async_start, \ ALREADY_DISCOVERED -from tests.common import async_fire_mqtt_message, mock_coro +from tests.common import async_fire_mqtt_message, mock_coro, MockConfigEntry @asyncio.coroutine def test_subscribing_config_topic(hass, mqtt_mock): """Test setting up discovery.""" + entry = MockConfigEntry(domain=mqtt.DOMAIN, data={ + mqtt.CONF_BROKER: 'test-broker' + }) + hass_config = {} discovery_topic = 'homeassistant' - yield from async_start(hass, discovery_topic, hass_config) + yield from async_start(hass, discovery_topic, hass_config, entry) assert mqtt_mock.async_subscribe.called call_args = mqtt_mock.async_subscribe.mock_calls[0][1] @@ -25,8 +30,12 @@ def test_subscribing_config_topic(hass, mqtt_mock): @asyncio.coroutine def test_invalid_topic(mock_load_platform, hass, mqtt_mock): """Test sending to invalid topic.""" + entry = MockConfigEntry(domain=mqtt.DOMAIN, data={ + mqtt.CONF_BROKER: 'test-broker' + }) + mock_load_platform.return_value = mock_coro() - yield from async_start(hass, 'homeassistant', {}) + yield from async_start(hass, 'homeassistant', {}, entry) async_fire_mqtt_message(hass, 'homeassistant/binary_sensor/bla/not_config', '{}') @@ -38,8 +47,12 @@ def test_invalid_topic(mock_load_platform, hass, mqtt_mock): @asyncio.coroutine def test_invalid_json(mock_load_platform, hass, mqtt_mock, caplog): """Test sending in invalid JSON.""" + entry = MockConfigEntry(domain=mqtt.DOMAIN, data={ + mqtt.CONF_BROKER: 'test-broker' + }) + mock_load_platform.return_value = mock_coro() - yield from async_start(hass, 'homeassistant', {}) + yield from async_start(hass, 'homeassistant', {}, entry) async_fire_mqtt_message(hass, 'homeassistant/binary_sensor/bla/config', 'not json') @@ -52,10 +65,12 @@ def test_invalid_json(mock_load_platform, hass, mqtt_mock, caplog): @asyncio.coroutine def test_only_valid_components(mock_load_platform, hass, mqtt_mock, caplog): """Test for a valid component.""" + entry = MockConfigEntry(domain=mqtt.DOMAIN) + invalid_component = "timer" mock_load_platform.return_value = mock_coro() - yield from async_start(hass, 'homeassistant', {}) + yield from async_start(hass, 'homeassistant', {}, entry) async_fire_mqtt_message(hass, 'homeassistant/{}/bla/config'.format( invalid_component @@ -73,7 +88,9 @@ def test_only_valid_components(mock_load_platform, hass, mqtt_mock, caplog): @asyncio.coroutine def test_correct_config_discovery(hass, mqtt_mock, caplog): """Test sending in correct JSON.""" - yield from async_start(hass, 'homeassistant', {}) + entry = MockConfigEntry(domain=mqtt.DOMAIN) + + yield from async_start(hass, 'homeassistant', {}, entry) async_fire_mqtt_message(hass, 'homeassistant/binary_sensor/bla/config', '{ "name": "Beer" }') @@ -89,7 +106,9 @@ def test_correct_config_discovery(hass, mqtt_mock, caplog): @asyncio.coroutine def test_discover_fan(hass, mqtt_mock, caplog): """Test discovering an MQTT fan.""" - yield from async_start(hass, 'homeassistant', {}) + entry = MockConfigEntry(domain=mqtt.DOMAIN) + + yield from async_start(hass, 'homeassistant', {}, entry) async_fire_mqtt_message(hass, 'homeassistant/fan/bla/config', ('{ "name": "Beer",' @@ -106,7 +125,9 @@ def test_discover_fan(hass, mqtt_mock, caplog): @asyncio.coroutine def test_discover_climate(hass, mqtt_mock, caplog): """Test discovering an MQTT climate component.""" - yield from async_start(hass, 'homeassistant', {}) + entry = MockConfigEntry(domain=mqtt.DOMAIN) + + yield from async_start(hass, 'homeassistant', {}, entry) data = ( '{ "name": "ClimateTest",' @@ -127,7 +148,9 @@ def test_discover_climate(hass, mqtt_mock, caplog): @asyncio.coroutine def test_discover_alarm_control_panel(hass, mqtt_mock, caplog): """Test discovering an MQTT alarm control panel component.""" - yield from async_start(hass, 'homeassistant', {}) + entry = MockConfigEntry(domain=mqtt.DOMAIN) + + yield from async_start(hass, 'homeassistant', {}, entry) data = ( '{ "name": "AlarmControlPanelTest",' @@ -149,7 +172,9 @@ def test_discover_alarm_control_panel(hass, mqtt_mock, caplog): @asyncio.coroutine def test_discovery_incl_nodeid(hass, mqtt_mock, caplog): """Test sending in correct JSON with optional node_id included.""" - yield from async_start(hass, 'homeassistant', {}) + entry = MockConfigEntry(domain=mqtt.DOMAIN) + + yield from async_start(hass, 'homeassistant', {}, entry) async_fire_mqtt_message(hass, 'homeassistant/binary_sensor/my_node_id/bla' '/config', '{ "name": "Beer" }') @@ -165,7 +190,9 @@ def test_discovery_incl_nodeid(hass, mqtt_mock, caplog): @asyncio.coroutine def test_non_duplicate_discovery(hass, mqtt_mock, caplog): """Test for a non duplicate component.""" - yield from async_start(hass, 'homeassistant', {}) + entry = MockConfigEntry(domain=mqtt.DOMAIN) + + yield from async_start(hass, 'homeassistant', {}, entry) async_fire_mqtt_message(hass, 'homeassistant/binary_sensor/bla/config', '{ "name": "Beer" }') diff --git a/tests/components/sensor/test_mqtt.py b/tests/components/sensor/test_mqtt.py index 527c92997ec..4f70c37e04f 100644 --- a/tests/components/sensor/test_mqtt.py +++ b/tests/components/sensor/test_mqtt.py @@ -6,6 +6,7 @@ from unittest.mock import patch import homeassistant.core as ha from homeassistant.setup import setup_component, async_setup_component +from homeassistant.components import mqtt from homeassistant.components.mqtt.discovery import async_start import homeassistant.components.sensor as sensor from homeassistant.const import EVENT_STATE_CHANGED, STATE_UNAVAILABLE @@ -13,7 +14,7 @@ import homeassistant.util.dt as dt_util from tests.common import mock_mqtt_component, fire_mqtt_message, \ assert_setup_component, async_fire_mqtt_message, \ - async_mock_mqtt_component + async_mock_mqtt_component, MockConfigEntry from tests.common import get_test_home_assistant, mock_component @@ -392,7 +393,8 @@ async def test_unique_id(hass): async def test_discovery_removal_sensor(hass, mqtt_mock, caplog): """Test removal of discovered sensor.""" - await async_start(hass, 'homeassistant', {}) + entry = MockConfigEntry(domain=mqtt.DOMAIN) + await async_start(hass, 'homeassistant', {}, entry) data = ( '{ "name": "Beer",' ' "status_topic": "test_topic" }' From 81e21b90c9dd42dd6dfae71f6325cbe78f77a715 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 27 Sep 2018 18:02:50 +0200 Subject: [PATCH 089/247] Fix auth redirect (#16914) * Fix auth redirect * Remove old test --- homeassistant/components/frontend/__init__.py | 7 ++++--- tests/components/frontend/test_init.py | 20 +++++++++++++------ 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 23ae6d485a6..c444ef2e3ca 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -344,11 +344,12 @@ class AuthorizeView(HomeAssistantView): _is_latest(self.js_option, request) if latest: - location = '/frontend_latest/authorize.html' + base = 'frontend_latest' else: - location = '/frontend_es5/authorize.html' + base = 'frontend_es5' - location += '?{}'.format(request.query_string) + location = "/{}/authorize.html{}".format( + base, str(request.url.relative())[15:]) return web.Response(status=302, headers={ 'location': location diff --git a/tests/components/frontend/test_init.py b/tests/components/frontend/test_init.py index e4daf686bfe..b29c8cfb12f 100644 --- a/tests/components/frontend/test_init.py +++ b/tests/components/frontend/test_init.py @@ -294,10 +294,18 @@ async def test_onboarding_load(mock_http_client): async def test_auth_authorize(mock_http_client): """Test the authorize endpoint works.""" - resp = await mock_http_client.get('/auth/authorize?hello=world') - assert resp.url.query_string == 'hello=world' - assert resp.url.path == '/frontend_es5/authorize.html' + resp = await mock_http_client.get( + '/auth/authorize?response_type=code&client_id=https://localhost/&' + 'redirect_uri=https://localhost/&state=123%23456') - resp = await mock_http_client.get('/auth/authorize?latest&hello=world') - assert resp.url.query_string == 'latest&hello=world' - assert resp.url.path == '/frontend_latest/authorize.html' + assert str(resp.url.relative()) == ( + '/frontend_es5/authorize.html?response_type=code&client_id=' + 'https://localhost/&redirect_uri=https://localhost/&state=123%23456') + + resp = await mock_http_client.get( + '/auth/authorize?latest&response_type=code&client_id=' + 'https://localhost/&redirect_uri=https://localhost/&state=123%23456') + + assert str(resp.url.relative()) == ( + '/frontend_latest/authorize.html?latest&response_type=code&client_id=' + 'https://localhost/&redirect_uri=https://localhost/&state=123%23456') From 2d104f95d77eb580ca9bd13966758742dbde099d Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Thu, 27 Sep 2018 22:56:04 +0200 Subject: [PATCH 090/247] Fix MQTT Config Entry Discovery (#16919) --- homeassistant/components/mqtt/__init__.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 70f20453633..335b4d31acb 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -321,7 +321,8 @@ async def _async_setup_server(hass: HomeAssistantType, config: ConfigType): async def _async_setup_discovery(hass: HomeAssistantType, conf: ConfigType, - hass_config: ConfigType) -> bool: + hass_config: ConfigType, + config_entry) -> bool: """Try to start the discovery of MQTT devices. This method is a coroutine. @@ -334,7 +335,8 @@ async def _async_setup_discovery(hass: HomeAssistantType, conf: ConfigType, return False success = await discovery.async_start( - hass, conf[CONF_DISCOVERY_PREFIX], hass_config) # type: bool + hass, conf[CONF_DISCOVERY_PREFIX], hass_config, + config_entry) # type: bool return success @@ -525,7 +527,7 @@ async def async_setup_entry(hass, entry): if conf.get(CONF_DISCOVERY): await _async_setup_discovery( - hass, conf, hass.data[DATA_MQTT_HASS_CONFIG]) + hass, conf, hass.data[DATA_MQTT_HASS_CONFIG], entry) return True From d77e88645e9b96b7cd86c08a836d9ab1b1afbe09 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 27 Sep 2018 23:04:03 +0200 Subject: [PATCH 091/247] Bump frontend to 20180927.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 c444ef2e3ca..023e75aac85 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==20180926.0'] +REQUIREMENTS = ['home-assistant-frontend==20180927.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log', diff --git a/requirements_all.txt b/requirements_all.txt index 6efd9a21d7e..eeee16a6138 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -455,7 +455,7 @@ hole==0.3.0 holidays==0.9.7 # homeassistant.components.frontend -home-assistant-frontend==20180926.0 +home-assistant-frontend==20180927.0 # homeassistant.components.homekit_controller # homekit==0.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1f7d58ef6aa..736310a719a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -88,7 +88,7 @@ hdate==0.6.3 holidays==0.9.7 # homeassistant.components.frontend -home-assistant-frontend==20180926.0 +home-assistant-frontend==20180927.0 # homeassistant.components.homematicip_cloud homematicip==0.9.8 From bac71d3d223d64223b16d9dc495ad3f1517981d7 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 27 Sep 2018 23:04:28 +0200 Subject: [PATCH 092/247] Update translations --- .../components/auth/.translations/de.json | 3 ++- .../components/auth/.translations/en.json | 6 +++--- .../components/auth/.translations/no.json | 19 +++++++++++++++++++ .../auth/.translations/zh-Hans.json | 4 ++-- .../components/deconz/.translations/de.json | 4 ++-- .../components/mqtt/.translations/de.json | 1 + .../components/mqtt/.translations/ko.json | 1 + .../components/mqtt/.translations/no.json | 1 + .../components/nest/.translations/de.json | 6 +++--- .../components/zone/.translations/de.json | 2 +- 10 files changed, 35 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/auth/.translations/de.json b/homeassistant/components/auth/.translations/de.json index 2abc64f5f5d..21c83290629 100644 --- a/homeassistant/components/auth/.translations/de.json +++ b/homeassistant/components/auth/.translations/de.json @@ -9,7 +9,8 @@ }, "step": { "init": { - "description": "Bitte w\u00e4hle einen der Benachrichtigungsdienste:" + "description": "Bitte w\u00e4hle 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:", diff --git a/homeassistant/components/auth/.translations/en.json b/homeassistant/components/auth/.translations/en.json index 21cb45e3050..66c0e92d9b5 100644 --- a/homeassistant/components/auth/.translations/en.json +++ b/homeassistant/components/auth/.translations/en.json @@ -2,18 +2,18 @@ "mfa_setup": { "notify": { "abort": { - "no_available_service": "No available notify services." + "no_available_service": "No notification services available." }, "error": { "invalid_code": "Invalid code, please try again." }, "step": { "init": { - "description": "Please select one of notify service:", + "description": "Please select one of the notification services:", "title": "Set up one-time password delivered by notify component" }, "setup": { - "description": "A one-time password have sent by **notify.{notify_service}**. Please input it in below:", + "description": "A one-time password has been sent via **notify.{notify_service}**. Please enter it below:", "title": "Verify setup" } }, diff --git a/homeassistant/components/auth/.translations/no.json b/homeassistant/components/auth/.translations/no.json index 43ec497cfb1..48b5db8a3b6 100644 --- a/homeassistant/components/auth/.translations/no.json +++ b/homeassistant/components/auth/.translations/no.json @@ -1,5 +1,24 @@ { "mfa_setup": { + "notify": { + "abort": { + "no_available_service": "Ingen varslingstjenester er tilgjengelig." + }, + "error": { + "invalid_code": "Ugyldig kode, vennligst pr\u00f8v igjen." + }, + "step": { + "init": { + "description": "Vennligst velg en av varslingstjenestene:", + "title": "Sett opp engangspassord levert av varsel komponent" + }, + "setup": { + "description": "Et engangspassord har blitt sendt via **notify.{notify_service}**. Vennligst skriv det inn nedenfor:", + "title": "Bekreft oppsettet" + } + }, + "title": "Varsle engangspassord" + }, "totp": { "error": { "invalid_code": "Ugyldig kode, pr\u00f8v igjen. Hvis du f\u00e5r denne feilen konsekvent, m\u00e5 du s\u00f8rge for at klokken p\u00e5 Home Assistant systemet er riktig." diff --git a/homeassistant/components/auth/.translations/zh-Hans.json b/homeassistant/components/auth/.translations/zh-Hans.json index d2a1b97b9b7..1cb311f016f 100644 --- a/homeassistant/components/auth/.translations/zh-Hans.json +++ b/homeassistant/components/auth/.translations/zh-Hans.json @@ -9,11 +9,11 @@ }, "step": { "init": { - "description": "\u8bf7\u9009\u62e9\u4ee5\u4e0b\u4e00\u4e2a\u901a\u77e5\u670d\u52a1\uff1a", + "description": "\u8bf7\u4ece\u4e0b\u9762\u9009\u62e9\u4e00\u4e2a\u901a\u77e5\u670d\u52a1\uff1a", "title": "\u8bbe\u7f6e\u7531\u901a\u77e5\u7ec4\u4ef6\u4f20\u9012\u7684\u4e00\u6b21\u6027\u5bc6\u7801" }, "setup": { - "description": "\u4e00\u6b21\u6027\u5bc6\u7801\u5df2\u7531 **notify.{notify_service}**\u53d1\u9001\u3002\u8bf7\u5728\u4e0b\u9762\u8f93\u5165:", + "description": "\u4e00\u6b21\u6027\u5bc6\u7801\u5df2\u7531 **notify.{notify_service}** \u53d1\u9001\u3002\u8bf7\u5728\u4e0b\u9762\u8f93\u5165\uff1a", "title": "\u9a8c\u8bc1\u8bbe\u7f6e" } }, diff --git a/homeassistant/components/deconz/.translations/de.json b/homeassistant/components/deconz/.translations/de.json index 51b496906a2..645daa56f6b 100644 --- a/homeassistant/components/deconz/.translations/de.json +++ b/homeassistant/components/deconz/.translations/de.json @@ -14,10 +14,10 @@ "host": "Host", "port": "Port (Standartwert : '80')" }, - "title": "Definieren Sie den deCONZ-Gateway" + "title": "Definiere das deCONZ-Gateway" }, "link": { - "description": "Entsperren Sie Ihr deCONZ-Gateway, um sich bei Home Assistant zu registrieren. \n\n 1. Gehen Sie zu den deCONZ-Systemeinstellungen \n 2. Dr\u00fccken Sie die Taste \"Gateway entsperren\"", + "description": "Entsperre dein deCONZ-Gateway, um dich bei Home Assistant zu registrieren. \n\n 1. Gehe zu den deCONZ-Systemeinstellungen \n 2. Dr\u00fccke die Taste \"Gateway entsperren\"", "title": "Mit deCONZ verbinden" }, "options": { diff --git a/homeassistant/components/mqtt/.translations/de.json b/homeassistant/components/mqtt/.translations/de.json index eeff1ca3041..2a35e95f559 100644 --- a/homeassistant/components/mqtt/.translations/de.json +++ b/homeassistant/components/mqtt/.translations/de.json @@ -9,6 +9,7 @@ "step": { "broker": { "data": { + "discovery": "Suche aktivieren", "password": "Passwort", "username": "Benutzername" }, diff --git a/homeassistant/components/mqtt/.translations/ko.json b/homeassistant/components/mqtt/.translations/ko.json index 3775c8328d1..f20658d252c 100644 --- a/homeassistant/components/mqtt/.translations/ko.json +++ b/homeassistant/components/mqtt/.translations/ko.json @@ -10,6 +10,7 @@ "broker": { "data": { "broker": "\ube0c\ub85c\ucee4", + "discovery": "\uae30\uae30 \uac80\uc0c9 \ud65c\uc131\ud654", "password": "\ube44\ubc00\ubc88\ud638", "port": "\ud3ec\ud2b8", "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" diff --git a/homeassistant/components/mqtt/.translations/no.json b/homeassistant/components/mqtt/.translations/no.json index a9c60fb0c7f..412efd3e107 100644 --- a/homeassistant/components/mqtt/.translations/no.json +++ b/homeassistant/components/mqtt/.translations/no.json @@ -10,6 +10,7 @@ "broker": { "data": { "broker": "Megler", + "discovery": "Aktiver oppdagelse", "password": "Passord", "port": "Port", "username": "Brukernavn" diff --git a/homeassistant/components/nest/.translations/de.json b/homeassistant/components/nest/.translations/de.json index 975d15e4470..500862039a2 100644 --- a/homeassistant/components/nest/.translations/de.json +++ b/homeassistant/components/nest/.translations/de.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "already_setup": "Sie k\u00f6nnen nur ein einziges Nest-Konto konfigurieren.", + "already_setup": "Du kannst nur ein einziges Nest-Konto konfigurieren.", "authorize_url_fail": "Unbekannter Fehler beim Erstellen der Authorisierungs-URL", "authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Authorisierungs-URL", - "no_flows": "Sie m\u00fcssen Nest konfigurieren, bevor Sie sich authentifizieren k\u00f6nnen. [Bitte lesen Sie die Anweisungen] (https://www.home-assistant.io/components/nest/)." + "no_flows": "Du musst Nest konfigurieren, bevor du dich authentifizieren kannst. [Bitte lese die Anweisungen] (https://www.home-assistant.io/components/nest/)." }, "error": { "internal_error": "Ein interner Fehler ist aufgetreten", @@ -17,7 +17,7 @@ "data": { "flow_impl": "Anbieter" }, - "description": "W\u00e4hlen Sie, \u00fcber welchen Authentifizierungsanbieter Sie sich bei Nest authentifizieren m\u00f6chten.", + "description": "W\u00e4hlen, \u00fcber welchen Authentifizierungsanbieter du dich bei Nest authentifizieren m\u00f6chtest.", "title": "Authentifizierungsanbieter" }, "link": { diff --git a/homeassistant/components/zone/.translations/de.json b/homeassistant/components/zone/.translations/de.json index fc1e3537f33..483c7f065a3 100644 --- a/homeassistant/components/zone/.translations/de.json +++ b/homeassistant/components/zone/.translations/de.json @@ -13,7 +13,7 @@ "passive": "Passiv", "radius": "Radius" }, - "title": "Definieren Sie die Zonenparameter" + "title": "Definiere die Zonenparameter" } }, "title": "Zone" From a7248d4574202bab7413b11ab54c95937ed5c026 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 27 Sep 2018 23:10:07 +0200 Subject: [PATCH 093/247] Handle exception handling websocket command (#16927) * Handle exception handling websocket command * lint * Lint2 --- homeassistant/components/websocket_api.py | 9 ++++++++- tests/components/test_websocket_api.py | 19 ++++++++++++++++++- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/websocket_api.py b/homeassistant/components/websocket_api.py index 9bd4aac4b6a..4e7c186facc 100644 --- a/homeassistant/components/websocket_api.py +++ b/homeassistant/components/websocket_api.py @@ -40,6 +40,7 @@ ERR_ID_REUSE = 1 ERR_INVALID_FORMAT = 2 ERR_NOT_FOUND = 3 ERR_UNKNOWN_COMMAND = 4 +ERR_UNKNOWN_ERROR = 5 TYPE_AUTH = 'auth' TYPE_AUTH_INVALID = 'auth_invalid' @@ -405,7 +406,13 @@ class ActiveConnection: else: handler, schema = handlers[msg['type']] - handler(self.hass, self, schema(msg)) + try: + handler(self.hass, self, schema(msg)) + except Exception: # pylint: disable=broad-except + _LOGGER.exception('Error handling message: %s', msg) + self.to_write.put_nowait(error_message( + cur_id, ERR_UNKNOWN_ERROR, + 'Unknown error.')) last_id = cur_id msg = await wsock.receive_json() diff --git a/tests/components/test_websocket_api.py b/tests/components/test_websocket_api.py index 199a9d804f8..cf74081adb1 100644 --- a/tests/components/test_websocket_api.py +++ b/tests/components/test_websocket_api.py @@ -1,6 +1,6 @@ """Tests for the Home Assistant Websocket API.""" import asyncio -from unittest.mock import patch +from unittest.mock import patch, Mock from aiohttp import WSMsgType from async_timeout import timeout @@ -539,3 +539,20 @@ async def test_call_service_context_no_user(hass, aiohttp_client): assert call.service == 'test_service' assert call.data == {'hello': 'world'} assert call.context.user_id is None + + +async def test_handler_failing(hass, websocket_client): + """Test a command that raises.""" + hass.components.websocket_api.async_register_command( + 'bla', Mock(side_effect=TypeError), + wapi.BASE_COMMAND_MESSAGE_SCHEMA.extend({'type': 'bla'})) + await websocket_client.send_json({ + 'id': 5, + 'type': 'bla', + }) + + msg = await websocket_client.receive_json() + assert msg['id'] == 5 + assert msg['type'] == wapi.TYPE_RESULT + assert not msg['success'] + assert msg['error']['code'] == wapi.ERR_UNKNOWN_ERROR From 70b901017fea68031709175297ae7db73aa03a7b Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Thu, 27 Sep 2018 23:13:11 +0200 Subject: [PATCH 094/247] Remove service helper (5) (#16917) * Update switch * Update script * Update light * Fix tests * Fix config/script hook * Async_create_task * Fix flux switch --- homeassistant/components/config/script.py | 9 +- .../components/device_sun_light_trigger.py | 27 ++++-- homeassistant/components/light/__init__.py | 85 ---------------- homeassistant/components/script.py | 36 ------- homeassistant/components/switch/__init__.py | 37 ------- homeassistant/components/switch/flux.py | 22 +++-- tests/components/light/common.py | 97 +++++++++++++++++++ tests/components/light/test_demo.py | 10 +- tests/components/light/test_group.py | 13 +-- tests/components/light/test_init.py | 51 +++++----- tests/components/light/test_litejet.py | 10 +- tests/components/light/test_mqtt.py | 34 ++++--- tests/components/light/test_mqtt_json.py | 26 ++--- tests/components/light/test_mqtt_template.py | 21 ++-- tests/components/light/test_template.py | 13 +-- tests/components/scene/test_init.py | 3 +- tests/components/switch/common.py | 39 ++++++++ tests/components/switch/test_command_line.py | 17 ++-- tests/components/switch/test_flux.py | 36 +++---- tests/components/switch/test_init.py | 9 +- tests/components/switch/test_litejet.py | 8 +- tests/components/switch/test_mqtt.py | 6 +- tests/components/switch/test_template.py | 6 +- tests/components/switch/test_wake_on_lan.py | 15 +-- .../test_device_sun_light_trigger.py | 7 +- tests/components/test_script.py | 60 ++++++++++-- tests/test_loader.py | 4 +- 27 files changed, 385 insertions(+), 316 deletions(-) create mode 100644 tests/components/light/common.py create mode 100644 tests/components/switch/common.py diff --git a/homeassistant/components/config/script.py b/homeassistant/components/config/script.py index 345c8e4a849..6f2eaa3ff67 100644 --- a/homeassistant/components/config/script.py +++ b/homeassistant/components/config/script.py @@ -2,7 +2,8 @@ import asyncio from homeassistant.components.config import EditKeyBasedConfigView -from homeassistant.components.script import SCRIPT_ENTRY_SCHEMA, async_reload +from homeassistant.components.script import DOMAIN, SCRIPT_ENTRY_SCHEMA +from homeassistant.const import SERVICE_RELOAD import homeassistant.helpers.config_validation as cv @@ -12,8 +13,12 @@ CONFIG_PATH = 'scripts.yaml' @asyncio.coroutine def async_setup(hass): """Set up the script config API.""" + async def hook(hass): + """post_write_hook for Config View that reloads scripts.""" + await hass.services.async_call(DOMAIN, SERVICE_RELOAD) + hass.http.register_view(EditKeyBasedConfigView( 'script', 'config', CONFIG_PATH, cv.slug, SCRIPT_ENTRY_SCHEMA, - post_write_hook=async_reload + post_write_hook=hook )) return True diff --git a/homeassistant/components/device_sun_light_trigger.py b/homeassistant/components/device_sun_light_trigger.py index 641ade7308b..b766513f1f4 100644 --- a/homeassistant/components/device_sun_light_trigger.py +++ b/homeassistant/components/device_sun_light_trigger.py @@ -12,7 +12,9 @@ import voluptuous as vol from homeassistant.core import callback import homeassistant.util.dt as dt_util -from homeassistant.const import STATE_HOME, STATE_NOT_HOME +from homeassistant.const import ( + SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_HOME, STATE_NOT_HOME) +from homeassistant.components.light import DOMAIN as DOMAIN_LIGHT from homeassistant.helpers.event import ( async_track_point_in_utc_time, async_track_state_change) from homeassistant.helpers.sun import is_up, get_astral_event_next @@ -86,9 +88,12 @@ def async_setup(hass, config): """Turn on lights.""" if not device_tracker.is_on() or light.is_on(light_id): return - light.async_turn_on(light_id, - transition=LIGHT_TRANSITION_TIME.seconds, - profile=light_profile) + hass.async_create_task( + hass.services.async_call( + DOMAIN_LIGHT, SERVICE_TURN_ON, dict( + entity_id=light_id, + transition=LIGHT_TRANSITION_TIME.seconds, + profile=light_profile))) def async_turn_on_factory(light_id): """Generate turn on callbacks as factory.""" @@ -138,7 +143,10 @@ def async_setup(hass, config): # Do we need lights? if light_needed: logger.info("Home coming event for %s. Turning lights on", entity) - light.async_turn_on(light_ids, profile=light_profile) + hass.async_create_task( + hass.services.async_call( + DOMAIN_LIGHT, SERVICE_TURN_ON, + dict(entity_id=light_ids, profile=light_profile))) # Are we in the time span were we would turn on the lights # if someone would be home? @@ -151,7 +159,10 @@ def async_setup(hass, config): # when the fading in started and turn it on if so for index, light_id in enumerate(light_ids): if now > start_point + index * LIGHT_TRANSITION_TIME: - light.async_turn_on(light_id) + hass.async_create_task( + hass.services.async_call( + DOMAIN_LIGHT, SERVICE_TURN_ON, + dict(entity_id=light_id))) else: # If this light didn't happen to be turned on yet so @@ -173,7 +184,9 @@ def async_setup(hass, config): logger.info( "Everyone has left but there are lights on. Turning them off") - light.async_turn_off(light_ids) + hass.async_create_task( + hass.services.async_call( + DOMAIN_LIGHT, SERVICE_TURN_OFF, dict(entity_id=light_ids))) async_track_state_change( hass, device_group, turn_off_lights_when_all_leave, diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index bc7f136322b..41dbbcd6d0c 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -17,7 +17,6 @@ from homeassistant.components.group import \ from homeassistant.const import ( ATTR_ENTITY_ID, SERVICE_TOGGLE, SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_ON) -from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa from homeassistant.helpers.entity import ToggleEntity @@ -142,90 +141,6 @@ def is_on(hass, entity_id=None): return hass.states.is_state(entity_id, STATE_ON) -@bind_hass -def turn_on(hass, entity_id=None, transition=None, brightness=None, - brightness_pct=None, rgb_color=None, xy_color=None, hs_color=None, - color_temp=None, kelvin=None, white_value=None, - profile=None, flash=None, effect=None, color_name=None): - """Turn all or specified light on.""" - hass.add_job( - async_turn_on, hass, entity_id, transition, brightness, brightness_pct, - rgb_color, xy_color, hs_color, color_temp, kelvin, white_value, - profile, flash, effect, color_name) - - -@callback -@bind_hass -def async_turn_on(hass, entity_id=None, transition=None, brightness=None, - brightness_pct=None, rgb_color=None, xy_color=None, - hs_color=None, color_temp=None, kelvin=None, - white_value=None, profile=None, flash=None, effect=None, - color_name=None): - """Turn all or specified light on.""" - data = { - key: value for key, value in [ - (ATTR_ENTITY_ID, entity_id), - (ATTR_PROFILE, profile), - (ATTR_TRANSITION, transition), - (ATTR_BRIGHTNESS, brightness), - (ATTR_BRIGHTNESS_PCT, brightness_pct), - (ATTR_RGB_COLOR, rgb_color), - (ATTR_XY_COLOR, xy_color), - (ATTR_HS_COLOR, hs_color), - (ATTR_COLOR_TEMP, color_temp), - (ATTR_KELVIN, kelvin), - (ATTR_WHITE_VALUE, white_value), - (ATTR_FLASH, flash), - (ATTR_EFFECT, effect), - (ATTR_COLOR_NAME, color_name), - ] if value is not None - } - - hass.async_add_job(hass.services.async_call(DOMAIN, SERVICE_TURN_ON, data)) - - -@bind_hass -def turn_off(hass, entity_id=None, transition=None): - """Turn all or specified light off.""" - hass.add_job(async_turn_off, hass, entity_id, transition) - - -@callback -@bind_hass -def async_turn_off(hass, entity_id=None, transition=None): - """Turn all or specified light off.""" - data = { - key: value for key, value in [ - (ATTR_ENTITY_ID, entity_id), - (ATTR_TRANSITION, transition), - ] if value is not None - } - - hass.async_add_job(hass.services.async_call( - DOMAIN, SERVICE_TURN_OFF, data)) - - -@callback -@bind_hass -def async_toggle(hass, entity_id=None, transition=None): - """Toggle all or specified light.""" - data = { - key: value for key, value in [ - (ATTR_ENTITY_ID, entity_id), - (ATTR_TRANSITION, transition), - ] if value is not None - } - - hass.async_add_job(hass.services.async_call( - DOMAIN, SERVICE_TOGGLE, data)) - - -@bind_hass -def toggle(hass, entity_id=None, transition=None): - """Toggle all or specified light.""" - hass.add_job(async_toggle, hass, entity_id, transition) - - def preprocess_turn_on_alternatives(params): """Process extra data for turn light on request.""" profile = Profiles.get(params.pop(ATTR_PROFILE, None)) diff --git a/homeassistant/components/script.py b/homeassistant/components/script.py index 247ac07283e..16c9f65420c 100644 --- a/homeassistant/components/script.py +++ b/homeassistant/components/script.py @@ -15,7 +15,6 @@ import voluptuous as vol from homeassistant.const import ( ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON, SERVICE_TOGGLE, SERVICE_RELOAD, STATE_ON, CONF_ALIAS) -from homeassistant.core import split_entity_id from homeassistant.loader import bind_hass from homeassistant.helpers.entity import ToggleEntity from homeassistant.helpers.entity_component import EntityComponent @@ -62,41 +61,6 @@ def is_on(hass, entity_id): return hass.states.is_state(entity_id, STATE_ON) -@bind_hass -def turn_on(hass, entity_id, variables=None, context=None): - """Turn script on.""" - _, object_id = split_entity_id(entity_id) - - hass.services.call(DOMAIN, object_id, variables, context=context) - - -@bind_hass -def turn_off(hass, entity_id): - """Turn script on.""" - hass.services.call(DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: entity_id}) - - -@bind_hass -def toggle(hass, entity_id): - """Toggle the script.""" - hass.services.call(DOMAIN, SERVICE_TOGGLE, {ATTR_ENTITY_ID: entity_id}) - - -@bind_hass -def reload(hass): - """Reload script component.""" - hass.services.call(DOMAIN, SERVICE_RELOAD) - - -@bind_hass -def async_reload(hass): - """Reload the scripts from config. - - Returns a coroutine object. - """ - return hass.services.async_call(DOMAIN, SERVICE_RELOAD) - - async def async_setup(hass, config): """Load the scripts from the configuration.""" component = EntityComponent( diff --git a/homeassistant/components/switch/__init__.py b/homeassistant/components/switch/__init__.py index c95c752435a..1adabe4b57e 100644 --- a/homeassistant/components/switch/__init__.py +++ b/homeassistant/components/switch/__init__.py @@ -9,7 +9,6 @@ import logging import voluptuous as vol -from homeassistant.core import callback from homeassistant.loader import bind_hass from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity import ToggleEntity @@ -56,42 +55,6 @@ def is_on(hass, entity_id=None): return hass.states.is_state(entity_id, STATE_ON) -@bind_hass -def turn_on(hass, entity_id=None): - """Turn all or specified switch 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 switch on.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else None - hass.async_add_job(hass.services.async_call(DOMAIN, SERVICE_TURN_ON, data)) - - -@bind_hass -def turn_off(hass, entity_id=None): - """Turn all or specified switch 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 switch off.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else None - hass.async_add_job( - hass.services.async_call(DOMAIN, SERVICE_TURN_OFF, data)) - - -@bind_hass -def toggle(hass, entity_id=None): - """Toggle all or specified switch.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else None - hass.services.call(DOMAIN, SERVICE_TOGGLE, data) - - async def async_setup(hass, config): """Track states and offer events for switches.""" component = hass.data[DOMAIN] = EntityComponent( diff --git a/homeassistant/components/switch/flux.py b/homeassistant/components/switch/flux.py index 15fdee59eaf..793d6ab91d0 100644 --- a/homeassistant/components/switch/flux.py +++ b/homeassistant/components/switch/flux.py @@ -13,10 +13,10 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.components.light import ( - is_on, turn_on, VALID_TRANSITION, ATTR_TRANSITION) + is_on, DOMAIN as LIGHT_DOMAIN, VALID_TRANSITION, ATTR_TRANSITION) from homeassistant.components.switch import DOMAIN, SwitchDevice from homeassistant.const import ( - CONF_NAME, CONF_PLATFORM, CONF_LIGHTS, CONF_MODE) + CONF_NAME, CONF_PLATFORM, CONF_LIGHTS, CONF_MODE, SERVICE_TURN_ON) from homeassistant.helpers.event import track_time_change from homeassistant.helpers.sun import get_astral_event_date from homeassistant.util import slugify @@ -69,30 +69,36 @@ def set_lights_xy(hass, lights, x_val, y_val, brightness, transition): """Set color of array of lights.""" for light in lights: if is_on(hass, light): - turn_on(hass, light, + hass.services.call( + LIGHT_DOMAIN, SERVICE_TURN_ON, dict( xy_color=[x_val, y_val], brightness=brightness, transition=transition, - white_value=brightness) + white_value=brightness, + entity_id=light)) def set_lights_temp(hass, lights, mired, brightness, transition): """Set color of array of lights.""" for light in lights: if is_on(hass, light): - turn_on(hass, light, + hass.services.call( + LIGHT_DOMAIN, SERVICE_TURN_ON, dict( color_temp=int(mired), brightness=brightness, - transition=transition) + transition=transition, + entity_id=light)) def set_lights_rgb(hass, lights, rgb, transition): """Set color of array of lights.""" for light in lights: if is_on(hass, light): - turn_on(hass, light, + hass.services.call( + LIGHT_DOMAIN, SERVICE_TURN_ON, dict( rgb_color=rgb, - transition=transition) + transition=transition, + entity_id=light)) def setup_platform(hass, config, add_entities, discovery_info=None): diff --git a/tests/components/light/common.py b/tests/components/light/common.py new file mode 100644 index 00000000000..906e0458dba --- /dev/null +++ b/tests/components/light/common.py @@ -0,0 +1,97 @@ +"""Collection of helper methods. + +All containing methods are legacy helpers that should not be used by new +components. Instead call the service directly. +""" +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, ATTR_BRIGHTNESS_PCT, ATTR_COLOR_NAME, ATTR_COLOR_TEMP, + ATTR_EFFECT, ATTR_FLASH, ATTR_HS_COLOR, ATTR_KELVIN, ATTR_PROFILE, + ATTR_RGB_COLOR, ATTR_TRANSITION, ATTR_WHITE_VALUE, ATTR_XY_COLOR, DOMAIN) +from homeassistant.const import ( + ATTR_ENTITY_ID, SERVICE_TOGGLE, SERVICE_TURN_OFF, SERVICE_TURN_ON) +from homeassistant.core import callback +from homeassistant.loader import bind_hass + + +@bind_hass +def turn_on(hass, entity_id=None, transition=None, brightness=None, + brightness_pct=None, rgb_color=None, xy_color=None, hs_color=None, + color_temp=None, kelvin=None, white_value=None, + profile=None, flash=None, effect=None, color_name=None): + """Turn all or specified light on.""" + hass.add_job( + async_turn_on, hass, entity_id, transition, brightness, brightness_pct, + rgb_color, xy_color, hs_color, color_temp, kelvin, white_value, + profile, flash, effect, color_name) + + +@callback +@bind_hass +def async_turn_on(hass, entity_id=None, transition=None, brightness=None, + brightness_pct=None, rgb_color=None, xy_color=None, + hs_color=None, color_temp=None, kelvin=None, + white_value=None, profile=None, flash=None, effect=None, + color_name=None): + """Turn all or specified light on.""" + data = { + key: value for key, value in [ + (ATTR_ENTITY_ID, entity_id), + (ATTR_PROFILE, profile), + (ATTR_TRANSITION, transition), + (ATTR_BRIGHTNESS, brightness), + (ATTR_BRIGHTNESS_PCT, brightness_pct), + (ATTR_RGB_COLOR, rgb_color), + (ATTR_XY_COLOR, xy_color), + (ATTR_HS_COLOR, hs_color), + (ATTR_COLOR_TEMP, color_temp), + (ATTR_KELVIN, kelvin), + (ATTR_WHITE_VALUE, white_value), + (ATTR_FLASH, flash), + (ATTR_EFFECT, effect), + (ATTR_COLOR_NAME, color_name), + ] if value is not None + } + + hass.async_add_job(hass.services.async_call(DOMAIN, SERVICE_TURN_ON, data)) + + +@bind_hass +def turn_off(hass, entity_id=None, transition=None): + """Turn all or specified light off.""" + hass.add_job(async_turn_off, hass, entity_id, transition) + + +@callback +@bind_hass +def async_turn_off(hass, entity_id=None, transition=None): + """Turn all or specified light off.""" + data = { + key: value for key, value in [ + (ATTR_ENTITY_ID, entity_id), + (ATTR_TRANSITION, transition), + ] if value is not None + } + + hass.async_add_job(hass.services.async_call( + DOMAIN, SERVICE_TURN_OFF, data)) + + +@bind_hass +def toggle(hass, entity_id=None, transition=None): + """Toggle all or specified light.""" + hass.add_job(async_toggle, hass, entity_id, transition) + + +@callback +@bind_hass +def async_toggle(hass, entity_id=None, transition=None): + """Toggle all or specified light.""" + data = { + key: value for key, value in [ + (ATTR_ENTITY_ID, entity_id), + (ATTR_TRANSITION, transition), + ] if value is not None + } + + hass.async_add_job(hass.services.async_call( + DOMAIN, SERVICE_TOGGLE, data)) diff --git a/tests/components/light/test_demo.py b/tests/components/light/test_demo.py index c61bd83cde2..8711acaa318 100644 --- a/tests/components/light/test_demo.py +++ b/tests/components/light/test_demo.py @@ -4,6 +4,8 @@ import pytest from homeassistant.setup import async_setup_component from homeassistant.components import light +from tests.components.light import common + ENTITY_LIGHT = 'light.bed_light' @@ -18,7 +20,7 @@ def setup_comp(hass): async def test_state_attributes(hass): """Test light state attributes.""" - light.async_turn_on( + common.async_turn_on( hass, ENTITY_LIGHT, xy_color=(.4, .4), brightness=25) await hass.async_block_till_done() state = hass.states.get(ENTITY_LIGHT) @@ -27,7 +29,7 @@ async def test_state_attributes(hass): assert 25 == state.attributes.get(light.ATTR_BRIGHTNESS) assert (255, 234, 164) == state.attributes.get(light.ATTR_RGB_COLOR) assert 'rainbow' == state.attributes.get(light.ATTR_EFFECT) - light.async_turn_on( + common.async_turn_on( hass, ENTITY_LIGHT, rgb_color=(251, 253, 255), white_value=254) await hass.async_block_till_done() @@ -35,14 +37,14 @@ async def test_state_attributes(hass): assert 254 == state.attributes.get(light.ATTR_WHITE_VALUE) assert (250, 252, 255) == state.attributes.get(light.ATTR_RGB_COLOR) assert (0.319, 0.326) == state.attributes.get(light.ATTR_XY_COLOR) - light.async_turn_on(hass, ENTITY_LIGHT, color_temp=400, effect='none') + common.async_turn_on(hass, ENTITY_LIGHT, color_temp=400, effect='none') await hass.async_block_till_done() state = hass.states.get(ENTITY_LIGHT) assert 400 == state.attributes.get(light.ATTR_COLOR_TEMP) assert 153 == state.attributes.get(light.ATTR_MIN_MIREDS) assert 500 == state.attributes.get(light.ATTR_MAX_MIREDS) assert 'none' == state.attributes.get(light.ATTR_EFFECT) - light.async_turn_on(hass, ENTITY_LIGHT, kelvin=3000, brightness_pct=50) + common.async_turn_on(hass, ENTITY_LIGHT, kelvin=3000, brightness_pct=50) await hass.async_block_till_done() state = hass.states.get(ENTITY_LIGHT) assert 333 == state.attributes.get(light.ATTR_COLOR_TEMP) diff --git a/tests/components/light/test_group.py b/tests/components/light/test_group.py index 4619e9fb9bd..472bdf42385 100644 --- a/tests/components/light/test_group.py +++ b/tests/components/light/test_group.py @@ -3,10 +3,11 @@ from unittest.mock import MagicMock import asynctest -from homeassistant.components import light from homeassistant.components.light import group from homeassistant.setup import async_setup_component +from tests.components.light import common + async def test_default_state(hass): """Test light group default state.""" @@ -300,29 +301,29 @@ async def test_service_calls(hass): await hass.async_block_till_done() assert hass.states.get('light.light_group').state == 'on' - light.async_toggle(hass, 'light.light_group') + common.async_toggle(hass, 'light.light_group') await hass.async_block_till_done() assert hass.states.get('light.bed_light').state == 'off' assert hass.states.get('light.ceiling_lights').state == 'off' assert hass.states.get('light.kitchen_lights').state == 'off' - light.async_turn_on(hass, 'light.light_group') + common.async_turn_on(hass, 'light.light_group') await hass.async_block_till_done() assert hass.states.get('light.bed_light').state == 'on' assert hass.states.get('light.ceiling_lights').state == 'on' assert hass.states.get('light.kitchen_lights').state == 'on' - light.async_turn_off(hass, 'light.light_group') + common.async_turn_off(hass, 'light.light_group') await hass.async_block_till_done() assert hass.states.get('light.bed_light').state == 'off' assert hass.states.get('light.ceiling_lights').state == 'off' assert hass.states.get('light.kitchen_lights').state == 'off' - light.async_turn_on(hass, 'light.light_group', brightness=128, - effect='Random', rgb_color=(42, 255, 255)) + common.async_turn_on(hass, 'light.light_group', brightness=128, + effect='Random', rgb_color=(42, 255, 255)) await hass.async_block_till_done() state = hass.states.get('light.bed_light') diff --git a/tests/components/light/test_init.py b/tests/components/light/test_init.py index 66dbadb5c38..6253de8cbae 100644 --- a/tests/components/light/test_init.py +++ b/tests/components/light/test_init.py @@ -15,6 +15,7 @@ from homeassistant.helpers.intent import IntentHandleError from tests.common import ( async_mock_service, mock_service, get_test_home_assistant, mock_storage) +from tests.components.light import common class TestLight(unittest.TestCase): @@ -54,7 +55,7 @@ class TestLight(unittest.TestCase): turn_on_calls = mock_service( self.hass, light.DOMAIN, SERVICE_TURN_ON) - light.turn_on( + common.turn_on( self.hass, entity_id='entity_id_val', transition='transition_val', @@ -88,7 +89,7 @@ class TestLight(unittest.TestCase): turn_off_calls = mock_service( self.hass, light.DOMAIN, SERVICE_TURN_OFF) - light.turn_off( + common.turn_off( self.hass, entity_id='entity_id_val', transition='transition_val') self.hass.block_till_done() @@ -105,7 +106,7 @@ class TestLight(unittest.TestCase): toggle_calls = mock_service( self.hass, light.DOMAIN, SERVICE_TOGGLE) - light.toggle( + common.toggle( self.hass, entity_id='entity_id_val', transition='transition_val') self.hass.block_till_done() @@ -135,8 +136,8 @@ class TestLight(unittest.TestCase): self.assertFalse(light.is_on(self.hass, dev3.entity_id)) # Test basic turn_on, turn_off, toggle services - light.turn_off(self.hass, entity_id=dev1.entity_id) - light.turn_on(self.hass, entity_id=dev2.entity_id) + common.turn_off(self.hass, entity_id=dev1.entity_id) + common.turn_on(self.hass, entity_id=dev2.entity_id) self.hass.block_till_done() @@ -144,7 +145,7 @@ class TestLight(unittest.TestCase): self.assertTrue(light.is_on(self.hass, dev2.entity_id)) # turn on all lights - light.turn_on(self.hass) + common.turn_on(self.hass) self.hass.block_till_done() @@ -153,7 +154,7 @@ class TestLight(unittest.TestCase): self.assertTrue(light.is_on(self.hass, dev3.entity_id)) # turn off all lights - light.turn_off(self.hass) + common.turn_off(self.hass) self.hass.block_till_done() @@ -162,7 +163,7 @@ class TestLight(unittest.TestCase): self.assertFalse(light.is_on(self.hass, dev3.entity_id)) # toggle all lights - light.toggle(self.hass) + common.toggle(self.hass) self.hass.block_till_done() @@ -171,7 +172,7 @@ class TestLight(unittest.TestCase): self.assertTrue(light.is_on(self.hass, dev3.entity_id)) # toggle all lights - light.toggle(self.hass) + common.toggle(self.hass) self.hass.block_till_done() @@ -180,12 +181,12 @@ class TestLight(unittest.TestCase): self.assertFalse(light.is_on(self.hass, dev3.entity_id)) # Ensure all attributes process correctly - light.turn_on(self.hass, dev1.entity_id, - transition=10, brightness=20, color_name='blue') - light.turn_on( + common.turn_on(self.hass, dev1.entity_id, + transition=10, brightness=20, color_name='blue') + common.turn_on( self.hass, dev2.entity_id, rgb_color=(255, 255, 255), white_value=255) - light.turn_on(self.hass, dev3.entity_id, xy_color=(.4, .6)) + common.turn_on(self.hass, dev3.entity_id, xy_color=(.4, .6)) self.hass.block_till_done() @@ -211,9 +212,9 @@ class TestLight(unittest.TestCase): prof_name, prof_h, prof_s, prof_bri = 'relax', 35.932, 69.412, 144 # Test light profiles - light.turn_on(self.hass, dev1.entity_id, profile=prof_name) + common.turn_on(self.hass, dev1.entity_id, profile=prof_name) # Specify a profile and a brightness attribute to overwrite it - light.turn_on( + common.turn_on( self.hass, dev2.entity_id, profile=prof_name, brightness=100) @@ -232,10 +233,10 @@ class TestLight(unittest.TestCase): }, data) # Test bad data - light.turn_on(self.hass) - light.turn_on(self.hass, dev1.entity_id, profile="nonexisting") - light.turn_on(self.hass, dev2.entity_id, xy_color=["bla-di-bla", 5]) - light.turn_on(self.hass, dev3.entity_id, rgb_color=[255, None, 2]) + common.turn_on(self.hass) + common.turn_on(self.hass, dev1.entity_id, profile="nonexisting") + common.turn_on(self.hass, dev2.entity_id, xy_color=["bla-di-bla", 5]) + common.turn_on(self.hass, dev3.entity_id, rgb_color=[255, None, 2]) self.hass.block_till_done() @@ -249,13 +250,13 @@ class TestLight(unittest.TestCase): self.assertEqual({}, data) # faulty attributes will not trigger a service call - light.turn_on( + common.turn_on( self.hass, dev1.entity_id, profile=prof_name, brightness='bright') - light.turn_on( + common.turn_on( self.hass, dev1.entity_id, rgb_color='yellowish') - light.turn_on( + common.turn_on( self.hass, dev2.entity_id, white_value='high') @@ -299,7 +300,7 @@ class TestLight(unittest.TestCase): dev1, _, _ = platform.DEVICES - light.turn_on(self.hass, dev1.entity_id, profile='test') + common.turn_on(self.hass, dev1.entity_id, profile='test') self.hass.block_till_done() @@ -340,7 +341,7 @@ class TestLight(unittest.TestCase): )) dev, _, _ = platform.DEVICES - light.turn_on(self.hass, dev.entity_id) + common.turn_on(self.hass, dev.entity_id) self.hass.block_till_done() _, data = dev.last_call('turn_on') self.assertEqual({ @@ -380,7 +381,7 @@ class TestLight(unittest.TestCase): dev = next(filter(lambda x: x.entity_id == 'light.ceiling_2', platform.DEVICES)) - light.turn_on(self.hass, dev.entity_id) + common.turn_on(self.hass, dev.entity_id) self.hass.block_till_done() _, data = dev.last_call('turn_on') self.assertEqual({ diff --git a/tests/components/light/test_litejet.py b/tests/components/light/test_litejet.py index 3040c95e0ac..1d7b8ea97fa 100644 --- a/tests/components/light/test_litejet.py +++ b/tests/components/light/test_litejet.py @@ -5,9 +5,11 @@ from unittest import mock from homeassistant import setup from homeassistant.components import litejet -from tests.common import get_test_home_assistant import homeassistant.components.light as light +from tests.common import get_test_home_assistant +from tests.components.light import common + _LOGGER = logging.getLogger(__name__) ENTITY_LIGHT = 'light.mock_load_1' @@ -78,7 +80,7 @@ class TestLiteJetLight(unittest.TestCase): assert not light.is_on(self.hass, ENTITY_LIGHT) - light.turn_on(self.hass, ENTITY_LIGHT, brightness=102) + common.turn_on(self.hass, ENTITY_LIGHT, brightness=102) self.hass.block_till_done() self.mock_lj.activate_load_at.assert_called_with( ENTITY_LIGHT_NUMBER, 39, 0) @@ -90,11 +92,11 @@ class TestLiteJetLight(unittest.TestCase): assert not light.is_on(self.hass, ENTITY_LIGHT) - light.turn_on(self.hass, ENTITY_LIGHT) + common.turn_on(self.hass, ENTITY_LIGHT) self.hass.block_till_done() self.mock_lj.activate_load.assert_called_with(ENTITY_LIGHT_NUMBER) - light.turn_off(self.hass, ENTITY_LIGHT) + common.turn_off(self.hass, ENTITY_LIGHT) self.hass.block_till_done() self.mock_lj.deactivate_load.assert_called_with(ENTITY_LIGHT_NUMBER) diff --git a/tests/components/light/test_mqtt.py b/tests/components/light/test_mqtt.py index 3640a6a0130..b4c76b6b895 100644 --- a/tests/components/light/test_mqtt.py +++ b/tests/components/light/test_mqtt.py @@ -148,9 +148,11 @@ from homeassistant.const import ( import homeassistant.components.light as light from homeassistant.components.mqtt.discovery import async_start import homeassistant.core as ha + from tests.common import ( assert_setup_component, get_test_home_assistant, mock_mqtt_component, async_fire_mqtt_message, fire_mqtt_message, mock_coro) +from tests.components.light import common class TestLightMQTT(unittest.TestCase): @@ -506,7 +508,7 @@ class TestLightMQTT(unittest.TestCase): self.assertEqual(50, state.attributes.get('white_value')) self.assertTrue(state.attributes.get(ATTR_ASSUMED_STATE)) - light.turn_on(self.hass, 'light.test') + common.turn_on(self.hass, 'light.test') self.hass.block_till_done() self.mock_publish.async_publish.assert_called_once_with( @@ -515,7 +517,7 @@ class TestLightMQTT(unittest.TestCase): state = self.hass.states.get('light.test') self.assertEqual(STATE_ON, state.state) - light.turn_off(self.hass, 'light.test') + common.turn_off(self.hass, 'light.test') self.hass.block_till_done() self.mock_publish.async_publish.assert_called_once_with( @@ -525,10 +527,10 @@ class TestLightMQTT(unittest.TestCase): self.assertEqual(STATE_OFF, state.state) self.mock_publish.reset_mock() - light.turn_on(self.hass, 'light.test', - brightness=50, xy_color=[0.123, 0.123]) - light.turn_on(self.hass, 'light.test', rgb_color=[255, 128, 0], - white_value=80) + common.turn_on(self.hass, 'light.test', + brightness=50, xy_color=[0.123, 0.123]) + common.turn_on(self.hass, 'light.test', rgb_color=[255, 128, 0], + white_value=80) self.hass.block_till_done() self.mock_publish.async_publish.assert_has_calls([ @@ -566,7 +568,7 @@ class TestLightMQTT(unittest.TestCase): state = self.hass.states.get('light.test') self.assertEqual(STATE_OFF, state.state) - light.turn_on(self.hass, 'light.test', rgb_color=[255, 128, 64]) + common.turn_on(self.hass, 'light.test', rgb_color=[255, 128, 64]) self.hass.block_till_done() self.mock_publish.async_publish.assert_has_calls([ @@ -714,7 +716,7 @@ class TestLightMQTT(unittest.TestCase): state = self.hass.states.get('light.test') self.assertEqual(STATE_OFF, state.state) - light.turn_on(self.hass, 'light.test', brightness=50) + common.turn_on(self.hass, 'light.test', brightness=50) self.hass.block_till_done() # Should get the following MQTT messages. @@ -726,7 +728,7 @@ class TestLightMQTT(unittest.TestCase): ], any_order=True) self.mock_publish.async_publish.reset_mock() - light.turn_off(self.hass, 'light.test') + common.turn_off(self.hass, 'light.test') self.hass.block_till_done() self.mock_publish.async_publish.assert_called_once_with( @@ -747,7 +749,7 @@ class TestLightMQTT(unittest.TestCase): state = self.hass.states.get('light.test') self.assertEqual(STATE_OFF, state.state) - light.turn_on(self.hass, 'light.test', brightness=50) + common.turn_on(self.hass, 'light.test', brightness=50) self.hass.block_till_done() # Should get the following MQTT messages. @@ -759,7 +761,7 @@ class TestLightMQTT(unittest.TestCase): ], any_order=True) self.mock_publish.async_publish.reset_mock() - light.turn_off(self.hass, 'light.test') + common.turn_off(self.hass, 'light.test') self.hass.block_till_done() self.mock_publish.async_publish.assert_called_once_with( @@ -783,7 +785,7 @@ class TestLightMQTT(unittest.TestCase): self.assertEqual(STATE_OFF, state.state) # Turn on w/ no brightness - should set to max - light.turn_on(self.hass, 'light.test') + common.turn_on(self.hass, 'light.test') self.hass.block_till_done() # Should get the following MQTT messages. @@ -792,7 +794,7 @@ class TestLightMQTT(unittest.TestCase): 'test_light/bright', 255, 0, False) self.mock_publish.async_publish.reset_mock() - light.turn_off(self.hass, 'light.test') + common.turn_off(self.hass, 'light.test') self.hass.block_till_done() self.mock_publish.async_publish.assert_called_once_with( @@ -800,19 +802,19 @@ class TestLightMQTT(unittest.TestCase): self.mock_publish.async_publish.reset_mock() # Turn on w/ brightness - light.turn_on(self.hass, 'light.test', brightness=50) + common.turn_on(self.hass, 'light.test', brightness=50) self.hass.block_till_done() self.mock_publish.async_publish.assert_called_once_with( 'test_light/bright', 50, 0, False) self.mock_publish.async_publish.reset_mock() - light.turn_off(self.hass, 'light.test') + common.turn_off(self.hass, 'light.test') self.hass.block_till_done() # Turn on w/ just a color to insure brightness gets # added and sent. - light.turn_on(self.hass, 'light.test', rgb_color=[255, 128, 0]) + common.turn_on(self.hass, 'light.test', rgb_color=[255, 128, 0]) self.hass.block_till_done() self.mock_publish.async_publish.assert_has_calls([ diff --git a/tests/components/light/test_mqtt_json.py b/tests/components/light/test_mqtt_json.py index 90875285f17..dbae4c53bfc 100644 --- a/tests/components/light/test_mqtt_json.py +++ b/tests/components/light/test_mqtt_json.py @@ -98,9 +98,11 @@ from homeassistant.const import ( ATTR_SUPPORTED_FEATURES) import homeassistant.components.light as light import homeassistant.core as ha + from tests.common import ( get_test_home_assistant, mock_mqtt_component, fire_mqtt_message, assert_setup_component, mock_coro) +from tests.components.light import common class TestLightMQTTJSON(unittest.TestCase): @@ -315,7 +317,7 @@ class TestLightMQTTJSON(unittest.TestCase): self.assertEqual(191, state.attributes.get(ATTR_SUPPORTED_FEATURES)) self.assertTrue(state.attributes.get(ATTR_ASSUMED_STATE)) - light.turn_on(self.hass, 'light.test') + common.turn_on(self.hass, 'light.test') self.hass.block_till_done() self.mock_publish.async_publish.assert_called_once_with( @@ -324,7 +326,7 @@ class TestLightMQTTJSON(unittest.TestCase): state = self.hass.states.get('light.test') self.assertEqual(STATE_ON, state.state) - light.turn_off(self.hass, 'light.test') + common.turn_off(self.hass, 'light.test') self.hass.block_till_done() self.mock_publish.async_publish.assert_called_once_with( @@ -333,9 +335,9 @@ class TestLightMQTTJSON(unittest.TestCase): state = self.hass.states.get('light.test') self.assertEqual(STATE_OFF, state.state) - light.turn_on(self.hass, 'light.test', - brightness=50, color_temp=155, effect='colorloop', - white_value=170) + common.turn_on(self.hass, 'light.test', + brightness=50, color_temp=155, effect='colorloop', + white_value=170) self.hass.block_till_done() self.assertEqual('test_light_rgb/set', @@ -361,8 +363,8 @@ class TestLightMQTTJSON(unittest.TestCase): self.assertEqual(170, state.attributes['white_value']) # Test a color command - light.turn_on(self.hass, 'light.test', - brightness=50, hs_color=(125, 100)) + common.turn_on(self.hass, 'light.test', + brightness=50, hs_color=(125, 100)) self.hass.block_till_done() self.assertEqual('test_light_rgb/set', @@ -398,7 +400,7 @@ class TestLightMQTTJSON(unittest.TestCase): } }) - light.turn_on(self.hass, 'light.test', hs_color=(180.0, 50.0)) + common.turn_on(self.hass, 'light.test', hs_color=(180.0, 50.0)) self.hass.block_till_done() message_json = json.loads( @@ -427,7 +429,7 @@ class TestLightMQTTJSON(unittest.TestCase): self.assertEqual(STATE_OFF, state.state) self.assertEqual(40, state.attributes.get(ATTR_SUPPORTED_FEATURES)) - light.turn_on(self.hass, 'light.test', flash="short") + common.turn_on(self.hass, 'light.test', flash="short") self.hass.block_till_done() self.assertEqual('test_light_rgb/set', @@ -443,7 +445,7 @@ class TestLightMQTTJSON(unittest.TestCase): self.assertEqual("ON", message_json["state"]) self.mock_publish.async_publish.reset_mock() - light.turn_on(self.hass, 'light.test', flash="long") + common.turn_on(self.hass, 'light.test', flash="long") self.hass.block_till_done() self.assertEqual('test_light_rgb/set', @@ -474,7 +476,7 @@ class TestLightMQTTJSON(unittest.TestCase): self.assertEqual(STATE_OFF, state.state) self.assertEqual(40, state.attributes.get(ATTR_SUPPORTED_FEATURES)) - light.turn_on(self.hass, 'light.test', transition=10) + common.turn_on(self.hass, 'light.test', transition=10) self.hass.block_till_done() self.assertEqual('test_light_rgb/set', @@ -490,7 +492,7 @@ class TestLightMQTTJSON(unittest.TestCase): self.assertEqual("ON", message_json["state"]) # Transition back off - light.turn_off(self.hass, 'light.test', transition=10) + common.turn_off(self.hass, 'light.test', transition=10) self.hass.block_till_done() self.assertEqual('test_light_rgb/set', diff --git a/tests/components/light/test_mqtt_template.py b/tests/components/light/test_mqtt_template.py index 8f92d659b9b..731e7cd4e45 100644 --- a/tests/components/light/test_mqtt_template.py +++ b/tests/components/light/test_mqtt_template.py @@ -34,9 +34,11 @@ from homeassistant.const import ( STATE_ON, STATE_OFF, STATE_UNAVAILABLE, ATTR_ASSUMED_STATE) import homeassistant.components.light as light import homeassistant.core as ha + from tests.common import ( get_test_home_assistant, mock_mqtt_component, fire_mqtt_message, assert_setup_component, mock_coro) +from tests.components.light import common class TestLightMQTTTemplate(unittest.TestCase): @@ -244,7 +246,7 @@ class TestLightMQTTTemplate(unittest.TestCase): self.assertTrue(state.attributes.get(ATTR_ASSUMED_STATE)) # turn on the light - light.turn_on(self.hass, 'light.test') + common.turn_on(self.hass, 'light.test') self.hass.block_till_done() self.mock_publish.async_publish.assert_called_once_with( @@ -254,7 +256,7 @@ class TestLightMQTTTemplate(unittest.TestCase): self.assertEqual(STATE_ON, state.state) # turn the light off - light.turn_off(self.hass, 'light.test') + common.turn_off(self.hass, 'light.test') self.hass.block_till_done() self.mock_publish.async_publish.assert_called_once_with( @@ -264,8 +266,8 @@ class TestLightMQTTTemplate(unittest.TestCase): self.assertEqual(STATE_OFF, state.state) # turn on the light with brightness, color - light.turn_on(self.hass, 'light.test', brightness=50, - rgb_color=[75, 75, 75]) + common.turn_on(self.hass, 'light.test', brightness=50, + rgb_color=[75, 75, 75]) self.hass.block_till_done() self.mock_publish.async_publish.assert_called_once_with( @@ -273,7 +275,8 @@ class TestLightMQTTTemplate(unittest.TestCase): self.mock_publish.async_publish.reset_mock() # turn on the light with color temp and white val - light.turn_on(self.hass, 'light.test', color_temp=200, white_value=139) + common.turn_on(self.hass, 'light.test', + color_temp=200, white_value=139) self.hass.block_till_done() self.mock_publish.async_publish.assert_called_once_with( @@ -305,7 +308,7 @@ class TestLightMQTTTemplate(unittest.TestCase): self.assertEqual(STATE_OFF, state.state) # short flash - light.turn_on(self.hass, 'light.test', flash='short') + common.turn_on(self.hass, 'light.test', flash='short') self.hass.block_till_done() self.mock_publish.async_publish.assert_called_once_with( @@ -313,7 +316,7 @@ class TestLightMQTTTemplate(unittest.TestCase): self.mock_publish.async_publish.reset_mock() # long flash - light.turn_on(self.hass, 'light.test', flash='long') + common.turn_on(self.hass, 'light.test', flash='long') self.hass.block_till_done() self.mock_publish.async_publish.assert_called_once_with( @@ -336,7 +339,7 @@ class TestLightMQTTTemplate(unittest.TestCase): self.assertEqual(STATE_OFF, state.state) # transition on - light.turn_on(self.hass, 'light.test', transition=10) + common.turn_on(self.hass, 'light.test', transition=10) self.hass.block_till_done() self.mock_publish.async_publish.assert_called_once_with( @@ -344,7 +347,7 @@ class TestLightMQTTTemplate(unittest.TestCase): self.mock_publish.async_publish.reset_mock() # transition off - light.turn_off(self.hass, 'light.test', transition=4) + common.turn_off(self.hass, 'light.test', transition=4) self.hass.block_till_done() self.mock_publish.async_publish.assert_called_once_with( diff --git a/tests/components/light/test_template.py b/tests/components/light/test_template.py index cc481fabb5c..5e4dd8555ae 100644 --- a/tests/components/light/test_template.py +++ b/tests/components/light/test_template.py @@ -3,12 +3,13 @@ import logging from homeassistant.core import callback from homeassistant import setup -import homeassistant.components as core from homeassistant.components.light import ATTR_BRIGHTNESS from homeassistant.const import STATE_ON, STATE_OFF from tests.common import ( get_test_home_assistant, assert_setup_component) +from tests.components.light import common + _LOGGER = logging.getLogger(__name__) @@ -378,7 +379,7 @@ class TestTemplateLight: state = self.hass.states.get('light.test_template_light') assert state.state == STATE_OFF - core.light.turn_on(self.hass, 'light.test_template_light') + common.turn_on(self.hass, 'light.test_template_light') self.hass.block_till_done() assert len(self.calls) == 1 @@ -418,7 +419,7 @@ class TestTemplateLight: state = self.hass.states.get('light.test_template_light') assert state.state == STATE_OFF - core.light.turn_on(self.hass, 'light.test_template_light') + common.turn_on(self.hass, 'light.test_template_light') self.hass.block_till_done() state = self.hass.states.get('light.test_template_light') @@ -461,7 +462,7 @@ class TestTemplateLight: state = self.hass.states.get('light.test_template_light') assert state.state == STATE_ON - core.light.turn_off(self.hass, 'light.test_template_light') + common.turn_off(self.hass, 'light.test_template_light') self.hass.block_till_done() assert len(self.calls) == 1 @@ -498,7 +499,7 @@ class TestTemplateLight: state = self.hass.states.get('light.test_template_light') assert state.state == STATE_OFF - core.light.turn_off(self.hass, 'light.test_template_light') + common.turn_off(self.hass, 'light.test_template_light') self.hass.block_till_done() assert len(self.calls) == 1 @@ -538,7 +539,7 @@ class TestTemplateLight: state = self.hass.states.get('light.test_template_light') assert state.attributes.get('brightness') is None - core.light.turn_on( + common.turn_on( self.hass, 'light.test_template_light', **{ATTR_BRIGHTNESS: 124}) self.hass.block_till_done() assert len(self.calls) == 1 diff --git a/tests/components/scene/test_init.py b/tests/components/scene/test_init.py index 08d9af95275..81ab5f89c25 100644 --- a/tests/components/scene/test_init.py +++ b/tests/components/scene/test_init.py @@ -8,6 +8,7 @@ from homeassistant.components import light, scene from homeassistant.util import yaml from tests.common import get_test_home_assistant +from tests.components.light import common as common_light from tests.components.scene import common @@ -26,7 +27,7 @@ class TestScene(unittest.TestCase): self.light_1, self.light_2 = test_light.DEVICES[0:2] - light.turn_off( + common_light.turn_off( self.hass, [self.light_1.entity_id, self.light_2.entity_id]) self.hass.block_till_done() diff --git a/tests/components/switch/common.py b/tests/components/switch/common.py new file mode 100644 index 00000000000..8db8e425ddb --- /dev/null +++ b/tests/components/switch/common.py @@ -0,0 +1,39 @@ +"""Collection of helper methods. + +All containing methods are legacy helpers that should not be used by new +components. Instead call the service directly. +""" +from homeassistant.components.switch import DOMAIN +from homeassistant.const import ( + ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON) +from homeassistant.core import callback +from homeassistant.loader import bind_hass + + +@bind_hass +def turn_on(hass, entity_id=None): + """Turn all or specified switch 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 switch on.""" + data = {ATTR_ENTITY_ID: entity_id} if entity_id else None + hass.async_add_job(hass.services.async_call(DOMAIN, SERVICE_TURN_ON, data)) + + +@bind_hass +def turn_off(hass, entity_id=None): + """Turn all or specified switch 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 switch off.""" + data = {ATTR_ENTITY_ID: entity_id} if entity_id else None + hass.async_add_job( + hass.services.async_call(DOMAIN, SERVICE_TURN_OFF, data)) diff --git a/tests/components/switch/test_command_line.py b/tests/components/switch/test_command_line.py index 9ec0507627d..a84281b4375 100644 --- a/tests/components/switch/test_command_line.py +++ b/tests/components/switch/test_command_line.py @@ -10,6 +10,7 @@ import homeassistant.components.switch as switch import homeassistant.components.switch.command_line as command_line from tests.common import get_test_home_assistant +from tests.components.switch import common # pylint: disable=invalid-name @@ -44,13 +45,13 @@ class TestCommandSwitch(unittest.TestCase): state = self.hass.states.get('switch.test') self.assertEqual(STATE_OFF, state.state) - switch.turn_on(self.hass, 'switch.test') + common.turn_on(self.hass, 'switch.test') self.hass.block_till_done() state = self.hass.states.get('switch.test') self.assertEqual(STATE_ON, state.state) - switch.turn_off(self.hass, 'switch.test') + common.turn_off(self.hass, 'switch.test') self.hass.block_till_done() state = self.hass.states.get('switch.test') @@ -78,13 +79,13 @@ class TestCommandSwitch(unittest.TestCase): state = self.hass.states.get('switch.test') self.assertEqual(STATE_OFF, state.state) - switch.turn_on(self.hass, 'switch.test') + common.turn_on(self.hass, 'switch.test') self.hass.block_till_done() state = self.hass.states.get('switch.test') self.assertEqual(STATE_ON, state.state) - switch.turn_off(self.hass, 'switch.test') + common.turn_off(self.hass, 'switch.test') self.hass.block_till_done() state = self.hass.states.get('switch.test') @@ -114,13 +115,13 @@ class TestCommandSwitch(unittest.TestCase): state = self.hass.states.get('switch.test') self.assertEqual(STATE_OFF, state.state) - switch.turn_on(self.hass, 'switch.test') + common.turn_on(self.hass, 'switch.test') self.hass.block_till_done() state = self.hass.states.get('switch.test') self.assertEqual(STATE_ON, state.state) - switch.turn_off(self.hass, 'switch.test') + common.turn_off(self.hass, 'switch.test') self.hass.block_till_done() state = self.hass.states.get('switch.test') @@ -147,13 +148,13 @@ class TestCommandSwitch(unittest.TestCase): state = self.hass.states.get('switch.test') self.assertEqual(STATE_OFF, state.state) - switch.turn_on(self.hass, 'switch.test') + common.turn_on(self.hass, 'switch.test') self.hass.block_till_done() state = self.hass.states.get('switch.test') self.assertEqual(STATE_ON, state.state) - switch.turn_off(self.hass, 'switch.test') + common.turn_off(self.hass, 'switch.test') self.hass.block_till_done() state = self.hass.states.get('switch.test') diff --git a/tests/components/switch/test_flux.py b/tests/components/switch/test_flux.py index f9ea88c5254..69e65e24659 100644 --- a/tests/components/switch/test_flux.py +++ b/tests/components/switch/test_flux.py @@ -11,6 +11,8 @@ import homeassistant.util.dt as dt_util from tests.common import ( assert_setup_component, get_test_home_assistant, fire_time_changed, mock_service) +from tests.components.light import common as common_light +from tests.components.switch import common class TestSwitchFlux(unittest.TestCase): @@ -147,7 +149,7 @@ class TestSwitchFlux(unittest.TestCase): }) turn_on_calls = mock_service( self.hass, light.DOMAIN, SERVICE_TURN_ON) - switch.turn_on(self.hass, 'switch.flux') + common.turn_on(self.hass, 'switch.flux') self.hass.block_till_done() fire_time_changed(self.hass, test_time) self.hass.block_till_done() @@ -193,7 +195,7 @@ class TestSwitchFlux(unittest.TestCase): }) turn_on_calls = mock_service( self.hass, light.DOMAIN, SERVICE_TURN_ON) - switch.turn_on(self.hass, 'switch.flux') + common.turn_on(self.hass, 'switch.flux') self.hass.block_till_done() fire_time_changed(self.hass, test_time) self.hass.block_till_done() @@ -240,7 +242,7 @@ class TestSwitchFlux(unittest.TestCase): }) turn_on_calls = mock_service( self.hass, light.DOMAIN, SERVICE_TURN_ON) - switch.turn_on(self.hass, 'switch.flux') + common.turn_on(self.hass, 'switch.flux') self.hass.block_till_done() fire_time_changed(self.hass, test_time) self.hass.block_till_done() @@ -286,7 +288,7 @@ class TestSwitchFlux(unittest.TestCase): }) turn_on_calls = mock_service( self.hass, light.DOMAIN, SERVICE_TURN_ON) - switch.turn_on(self.hass, 'switch.flux') + common.turn_on(self.hass, 'switch.flux') self.hass.block_till_done() fire_time_changed(self.hass, test_time) self.hass.block_till_done() @@ -334,7 +336,7 @@ class TestSwitchFlux(unittest.TestCase): }) turn_on_calls = mock_service( self.hass, light.DOMAIN, SERVICE_TURN_ON) - switch.turn_on(self.hass, 'switch.flux') + common.turn_on(self.hass, 'switch.flux') self.hass.block_till_done() fire_time_changed(self.hass, test_time) self.hass.block_till_done() @@ -383,7 +385,7 @@ class TestSwitchFlux(unittest.TestCase): }) turn_on_calls = mock_service( self.hass, light.DOMAIN, SERVICE_TURN_ON) - switch.turn_on(self.hass, 'switch.flux') + common.turn_on(self.hass, 'switch.flux') self.hass.block_till_done() fire_time_changed(self.hass, test_time) self.hass.block_till_done() @@ -434,7 +436,7 @@ class TestSwitchFlux(unittest.TestCase): }) turn_on_calls = mock_service( self.hass, light.DOMAIN, SERVICE_TURN_ON) - switch.turn_on(self.hass, 'switch.flux') + common.turn_on(self.hass, 'switch.flux') self.hass.block_till_done() fire_time_changed(self.hass, test_time) self.hass.block_till_done() @@ -484,7 +486,7 @@ class TestSwitchFlux(unittest.TestCase): }) turn_on_calls = mock_service( self.hass, light.DOMAIN, SERVICE_TURN_ON) - switch.turn_on(self.hass, 'switch.flux') + common.turn_on(self.hass, 'switch.flux') self.hass.block_till_done() fire_time_changed(self.hass, test_time) self.hass.block_till_done() @@ -534,7 +536,7 @@ class TestSwitchFlux(unittest.TestCase): }) turn_on_calls = mock_service( self.hass, light.DOMAIN, SERVICE_TURN_ON) - switch.turn_on(self.hass, 'switch.flux') + common.turn_on(self.hass, 'switch.flux') self.hass.block_till_done() fire_time_changed(self.hass, test_time) self.hass.block_till_done() @@ -584,7 +586,7 @@ class TestSwitchFlux(unittest.TestCase): }) turn_on_calls = mock_service( self.hass, light.DOMAIN, SERVICE_TURN_ON) - switch.turn_on(self.hass, 'switch.flux') + common.turn_on(self.hass, 'switch.flux') self.hass.block_till_done() fire_time_changed(self.hass, test_time) self.hass.block_till_done() @@ -633,7 +635,7 @@ class TestSwitchFlux(unittest.TestCase): }) turn_on_calls = mock_service( self.hass, light.DOMAIN, SERVICE_TURN_ON) - switch.turn_on(self.hass, 'switch.flux') + common.turn_on(self.hass, 'switch.flux') self.hass.block_till_done() fire_time_changed(self.hass, test_time) self.hass.block_till_done() @@ -681,7 +683,7 @@ class TestSwitchFlux(unittest.TestCase): }) turn_on_calls = mock_service( self.hass, light.DOMAIN, SERVICE_TURN_ON) - switch.turn_on(self.hass, 'switch.flux') + common.turn_on(self.hass, 'switch.flux') self.hass.block_till_done() fire_time_changed(self.hass, test_time) self.hass.block_till_done() @@ -698,9 +700,9 @@ class TestSwitchFlux(unittest.TestCase): {light.DOMAIN: {CONF_PLATFORM: 'test'}})) dev1, dev2, dev3 = platform.DEVICES - light.turn_on(self.hass, entity_id=dev2.entity_id) + common_light.turn_on(self.hass, entity_id=dev2.entity_id) self.hass.block_till_done() - light.turn_on(self.hass, entity_id=dev3.entity_id) + common_light.turn_on(self.hass, entity_id=dev3.entity_id) self.hass.block_till_done() state = self.hass.states.get(dev1.entity_id) @@ -743,7 +745,7 @@ class TestSwitchFlux(unittest.TestCase): }) turn_on_calls = mock_service( self.hass, light.DOMAIN, SERVICE_TURN_ON) - switch.turn_on(self.hass, 'switch.flux') + common.turn_on(self.hass, 'switch.flux') self.hass.block_till_done() fire_time_changed(self.hass, test_time) self.hass.block_till_done() @@ -794,7 +796,7 @@ class TestSwitchFlux(unittest.TestCase): }) turn_on_calls = mock_service( self.hass, light.DOMAIN, SERVICE_TURN_ON) - switch.turn_on(self.hass, 'switch.flux') + common.turn_on(self.hass, 'switch.flux') self.hass.block_till_done() fire_time_changed(self.hass, test_time) self.hass.block_till_done() @@ -838,7 +840,7 @@ class TestSwitchFlux(unittest.TestCase): }) turn_on_calls = mock_service( self.hass, light.DOMAIN, SERVICE_TURN_ON) - switch.turn_on(self.hass, 'switch.flux') + common.turn_on(self.hass, 'switch.flux') self.hass.block_till_done() fire_time_changed(self.hass, test_time) self.hass.block_till_done() diff --git a/tests/components/switch/test_init.py b/tests/components/switch/test_init.py index 579898437ca..a7462eecd42 100644 --- a/tests/components/switch/test_init.py +++ b/tests/components/switch/test_init.py @@ -8,6 +8,7 @@ from homeassistant.components import switch from homeassistant.const import STATE_ON, STATE_OFF, CONF_PLATFORM from tests.common import get_test_home_assistant +from tests.components.switch import common class TestSwitch(unittest.TestCase): @@ -41,8 +42,8 @@ class TestSwitch(unittest.TestCase): self.assertFalse(switch.is_on(self.hass, self.switch_2.entity_id)) self.assertFalse(switch.is_on(self.hass, self.switch_3.entity_id)) - switch.turn_off(self.hass, self.switch_1.entity_id) - switch.turn_on(self.hass, self.switch_2.entity_id) + common.turn_off(self.hass, self.switch_1.entity_id) + common.turn_on(self.hass, self.switch_2.entity_id) self.hass.block_till_done() @@ -51,7 +52,7 @@ class TestSwitch(unittest.TestCase): self.assertTrue(switch.is_on(self.hass, self.switch_2.entity_id)) # Turn all off - switch.turn_off(self.hass) + common.turn_off(self.hass) self.hass.block_till_done() @@ -64,7 +65,7 @@ class TestSwitch(unittest.TestCase): self.assertFalse(switch.is_on(self.hass, self.switch_3.entity_id)) # Turn all on - switch.turn_on(self.hass) + common.turn_on(self.hass) self.hass.block_till_done() diff --git a/tests/components/switch/test_litejet.py b/tests/components/switch/test_litejet.py index 45e5509c169..f1d23f48b86 100644 --- a/tests/components/switch/test_litejet.py +++ b/tests/components/switch/test_litejet.py @@ -5,9 +5,11 @@ from unittest import mock from homeassistant import setup from homeassistant.components import litejet -from tests.common import get_test_home_assistant import homeassistant.components.switch as switch +from tests.common import get_test_home_assistant +from tests.components.switch import common + _LOGGER = logging.getLogger(__name__) ENTITY_SWITCH = 'switch.mock_switch_1' @@ -88,11 +90,11 @@ class TestLiteJetSwitch(unittest.TestCase): assert not switch.is_on(self.hass, ENTITY_SWITCH) - switch.turn_on(self.hass, ENTITY_SWITCH) + common.turn_on(self.hass, ENTITY_SWITCH) self.hass.block_till_done() self.mock_lj.press_switch.assert_called_with(ENTITY_SWITCH_NUMBER) - switch.turn_off(self.hass, ENTITY_SWITCH) + common.turn_off(self.hass, ENTITY_SWITCH) self.hass.block_till_done() self.mock_lj.release_switch.assert_called_with(ENTITY_SWITCH_NUMBER) diff --git a/tests/components/switch/test_mqtt.py b/tests/components/switch/test_mqtt.py index cad93e3bfce..ac113a05829 100644 --- a/tests/components/switch/test_mqtt.py +++ b/tests/components/switch/test_mqtt.py @@ -8,9 +8,11 @@ from homeassistant.const import STATE_ON, STATE_OFF, STATE_UNAVAILABLE,\ import homeassistant.core as ha import homeassistant.components.switch as switch from homeassistant.components.mqtt.discovery import async_start + from tests.common import ( mock_mqtt_component, fire_mqtt_message, get_test_home_assistant, mock_coro, async_mock_mqtt_component, async_fire_mqtt_message) +from tests.components.switch import common class TestSwitchMQTT(unittest.TestCase): @@ -75,7 +77,7 @@ class TestSwitchMQTT(unittest.TestCase): self.assertEqual(STATE_ON, state.state) self.assertTrue(state.attributes.get(ATTR_ASSUMED_STATE)) - switch.turn_on(self.hass, 'switch.test') + common.turn_on(self.hass, 'switch.test') self.hass.block_till_done() self.mock_publish.async_publish.assert_called_once_with( @@ -84,7 +86,7 @@ class TestSwitchMQTT(unittest.TestCase): state = self.hass.states.get('switch.test') self.assertEqual(STATE_ON, state.state) - switch.turn_off(self.hass, 'switch.test') + common.turn_off(self.hass, 'switch.test') self.hass.block_till_done() self.mock_publish.async_publish.assert_called_once_with( diff --git a/tests/components/switch/test_template.py b/tests/components/switch/test_template.py index 47766e31f4d..1492aab250f 100644 --- a/tests/components/switch/test_template.py +++ b/tests/components/switch/test_template.py @@ -1,11 +1,11 @@ """The tests for the Template switch platform.""" from homeassistant.core import callback from homeassistant import setup -import homeassistant.components as core from homeassistant.const import STATE_ON, STATE_OFF from tests.common import ( get_test_home_assistant, assert_setup_component) +from tests.components.switch import common class TestTemplateSwitch: @@ -406,7 +406,7 @@ class TestTemplateSwitch: state = self.hass.states.get('switch.test_template_switch') assert state.state == STATE_OFF - core.switch.turn_on(self.hass, 'switch.test_template_switch') + common.turn_on(self.hass, 'switch.test_template_switch') self.hass.block_till_done() assert len(self.calls) == 1 @@ -442,7 +442,7 @@ class TestTemplateSwitch: state = self.hass.states.get('switch.test_template_switch') assert state.state == STATE_ON - core.switch.turn_off(self.hass, 'switch.test_template_switch') + common.turn_off(self.hass, 'switch.test_template_switch') self.hass.block_till_done() assert len(self.calls) == 1 diff --git a/tests/components/switch/test_wake_on_lan.py b/tests/components/switch/test_wake_on_lan.py index abe1532cec7..42ebd1ff231 100644 --- a/tests/components/switch/test_wake_on_lan.py +++ b/tests/components/switch/test_wake_on_lan.py @@ -7,6 +7,7 @@ from homeassistant.const import STATE_ON, STATE_OFF import homeassistant.components.switch as switch from tests.common import get_test_home_assistant, mock_service +from tests.components.switch import common TEST_STATE = None @@ -59,13 +60,13 @@ class TestWOLSwitch(unittest.TestCase): TEST_STATE = True - switch.turn_on(self.hass, 'switch.wake_on_lan') + common.turn_on(self.hass, 'switch.wake_on_lan') self.hass.block_till_done() state = self.hass.states.get('switch.wake_on_lan') self.assertEqual(STATE_ON, state.state) - switch.turn_off(self.hass, 'switch.wake_on_lan') + common.turn_off(self.hass, 'switch.wake_on_lan') self.hass.block_till_done() state = self.hass.states.get('switch.wake_on_lan') @@ -91,7 +92,7 @@ class TestWOLSwitch(unittest.TestCase): TEST_STATE = True - switch.turn_on(self.hass, 'switch.wake_on_lan') + common.turn_on(self.hass, 'switch.wake_on_lan') self.hass.block_till_done() state = self.hass.states.get('switch.wake_on_lan') @@ -123,7 +124,7 @@ class TestWOLSwitch(unittest.TestCase): state = self.hass.states.get('switch.wake_on_lan') self.assertEqual(STATE_OFF, state.state) - switch.turn_on(self.hass, 'switch.wake_on_lan') + common.turn_on(self.hass, 'switch.wake_on_lan') self.hass.block_till_done() @patch('wakeonlan.send_magic_packet', new=send_magic_packet) @@ -149,7 +150,7 @@ class TestWOLSwitch(unittest.TestCase): TEST_STATE = True - switch.turn_on(self.hass, 'switch.wake_on_lan') + common.turn_on(self.hass, 'switch.wake_on_lan') self.hass.block_till_done() state = self.hass.states.get('switch.wake_on_lan') @@ -158,7 +159,7 @@ class TestWOLSwitch(unittest.TestCase): TEST_STATE = False - switch.turn_off(self.hass, 'switch.wake_on_lan') + common.turn_off(self.hass, 'switch.wake_on_lan') self.hass.block_till_done() state = self.hass.states.get('switch.wake_on_lan') @@ -185,7 +186,7 @@ class TestWOLSwitch(unittest.TestCase): TEST_STATE = True - switch.turn_on(self.hass, 'switch.wake_on_lan') + common.turn_on(self.hass, 'switch.wake_on_lan') self.hass.block_till_done() state = self.hass.states.get('switch.wake_on_lan') diff --git a/tests/components/test_device_sun_light_trigger.py b/tests/components/test_device_sun_light_trigger.py index 35d53f9a5c8..7107eee74fe 100644 --- a/tests/components/test_device_sun_light_trigger.py +++ b/tests/components/test_device_sun_light_trigger.py @@ -12,6 +12,7 @@ from homeassistant.components import ( from homeassistant.util import dt as dt_util from tests.common import get_test_home_assistant, fire_time_changed +from tests.components.light import common as common_light class TestDeviceSunLightTrigger(unittest.TestCase): @@ -68,7 +69,7 @@ class TestDeviceSunLightTrigger(unittest.TestCase): self.hass, device_sun_light_trigger.DOMAIN, { device_sun_light_trigger.DOMAIN: {}})) - light.turn_off(self.hass) + common_light.turn_off(self.hass) self.hass.block_till_done() @@ -81,7 +82,7 @@ class TestDeviceSunLightTrigger(unittest.TestCase): def test_lights_turn_off_when_everyone_leaves(self): """Test lights turn off when everyone leaves the house.""" - light.turn_on(self.hass) + common_light.turn_on(self.hass) self.hass.block_till_done() @@ -100,7 +101,7 @@ class TestDeviceSunLightTrigger(unittest.TestCase): """Test lights turn on when coming home after sun set.""" test_time = datetime(2017, 4, 5, 3, 2, 3, tzinfo=dt_util.UTC) with patch('homeassistant.util.dt.utcnow', return_value=test_time): - light.turn_off(self.hass) + common_light.turn_off(self.hass) self.hass.block_till_done() self.assertTrue(setup_component( diff --git a/tests/components/test_script.py b/tests/components/test_script.py index b9aa921cb63..43727b6d559 100644 --- a/tests/components/test_script.py +++ b/tests/components/test_script.py @@ -3,9 +3,13 @@ import unittest from unittest.mock import patch -from homeassistant.core import Context, callback -from homeassistant.setup import setup_component from homeassistant.components import script +from homeassistant.components.script import DOMAIN +from homeassistant.const import ( + ATTR_ENTITY_ID, SERVICE_RELOAD, SERVICE_TOGGLE, SERVICE_TURN_OFF) +from homeassistant.core import Context, callback, split_entity_id +from homeassistant.loader import bind_hass +from homeassistant.setup import setup_component from tests.common import get_test_home_assistant @@ -13,6 +17,44 @@ from tests.common import get_test_home_assistant ENTITY_ID = 'script.test' +@bind_hass +def turn_on(hass, entity_id, variables=None, context=None): + """Turn script on. + + This is a legacy helper method. Do not use it for new tests. + """ + _, object_id = split_entity_id(entity_id) + + hass.services.call(DOMAIN, object_id, variables, context=context) + + +@bind_hass +def turn_off(hass, entity_id): + """Turn script on. + + This is a legacy helper method. Do not use it for new tests. + """ + hass.services.call(DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: entity_id}) + + +@bind_hass +def toggle(hass, entity_id): + """Toggle the script. + + This is a legacy helper method. Do not use it for new tests. + """ + hass.services.call(DOMAIN, SERVICE_TOGGLE, {ATTR_ENTITY_ID: entity_id}) + + +@bind_hass +def reload(hass): + """Reload script component. + + This is a legacy helper method. Do not use it for new tests. + """ + hass.services.call(DOMAIN, SERVICE_RELOAD) + + class TestScriptComponent(unittest.TestCase): """Test the Script component.""" @@ -76,17 +118,17 @@ class TestScriptComponent(unittest.TestCase): } }) - script.turn_on(self.hass, ENTITY_ID) + turn_on(self.hass, ENTITY_ID) self.hass.block_till_done() self.assertTrue(script.is_on(self.hass, ENTITY_ID)) self.assertEqual(0, len(events)) # Calling turn_on a second time should not advance the script - script.turn_on(self.hass, ENTITY_ID) + turn_on(self.hass, ENTITY_ID) self.hass.block_till_done() self.assertEqual(0, len(events)) - script.turn_off(self.hass, ENTITY_ID) + turn_off(self.hass, ENTITY_ID) self.hass.block_till_done() self.assertFalse(script.is_on(self.hass, ENTITY_ID)) self.assertEqual(0, len(events)) @@ -121,12 +163,12 @@ class TestScriptComponent(unittest.TestCase): } }) - script.toggle(self.hass, ENTITY_ID) + toggle(self.hass, ENTITY_ID) self.hass.block_till_done() self.assertTrue(script.is_on(self.hass, ENTITY_ID)) self.assertEqual(0, len(events)) - script.toggle(self.hass, ENTITY_ID) + toggle(self.hass, ENTITY_ID) self.hass.block_till_done() self.assertFalse(script.is_on(self.hass, ENTITY_ID)) self.assertEqual(0, len(events)) @@ -156,7 +198,7 @@ class TestScriptComponent(unittest.TestCase): }, }) - script.turn_on(self.hass, ENTITY_ID, { + turn_on(self.hass, ENTITY_ID, { 'greeting': 'world' }, context=context) @@ -204,7 +246,7 @@ class TestScriptComponent(unittest.TestCase): }}}): with patch('homeassistant.config.find_config_file', return_value=''): - script.reload(self.hass) + reload(self.hass) self.hass.block_till_done() assert self.hass.states.get(ENTITY_ID) is None diff --git a/tests/test_loader.py b/tests/test_loader.py index 4beb7db570e..c4adb971593 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -79,10 +79,10 @@ def test_component_loader_non_existing(hass): @asyncio.coroutine def test_component_wrapper(hass): """Test component wrapper.""" - calls = async_mock_service(hass, 'light', 'turn_on') + calls = async_mock_service(hass, 'persistent_notification', 'create') components = loader.Components(hass) - components.light.async_turn_on('light.test') + components.persistent_notification.async_create('message') yield from hass.async_block_till_done() assert len(calls) == 1 From f879ac0993f0650ce89dbc513362a2663c0ee528 Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Thu, 27 Sep 2018 23:14:09 +0200 Subject: [PATCH 095/247] Remove service helper (6) (#16920) * Update automation * Update group * Async_create_task --- .../components/automation/__init__.py | 15 ----- homeassistant/components/config/automation.py | 11 ++-- .../components/device_tracker/__init__.py | 22 ++++--- homeassistant/components/group/__init__.py | 57 ------------------ homeassistant/helpers/entity_component.py | 15 +++-- tests/components/group/common.py | 60 ++++++++++++++++++- tests/components/group/test_init.py | 12 ++-- 7 files changed, 95 insertions(+), 97 deletions(-) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index e657409ea01..b34a6b7cf40 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -115,21 +115,6 @@ def is_on(hass, entity_id): return hass.states.is_state(entity_id, STATE_ON) -@bind_hass -def reload(hass): - """Reload the automation from config.""" - hass.services.call(DOMAIN, SERVICE_RELOAD) - - -@bind_hass -def async_reload(hass): - """Reload the automation from config. - - Returns a coroutine object. - """ - return hass.services.async_call(DOMAIN, SERVICE_RELOAD) - - async def async_setup(hass, config): """Set up the automation.""" component = EntityComponent(_LOGGER, DOMAIN, hass, diff --git a/homeassistant/components/config/automation.py b/homeassistant/components/config/automation.py index 223159eb415..2d13ac07025 100644 --- a/homeassistant/components/config/automation.py +++ b/homeassistant/components/config/automation.py @@ -3,10 +3,9 @@ import asyncio from collections import OrderedDict import uuid -from homeassistant.const import CONF_ID from homeassistant.components.config import EditIdBasedConfigView -from homeassistant.components.automation import ( - PLATFORM_SCHEMA, DOMAIN, async_reload) +from homeassistant.const import CONF_ID, SERVICE_RELOAD +from homeassistant.components.automation import DOMAIN, PLATFORM_SCHEMA import homeassistant.helpers.config_validation as cv @@ -16,9 +15,13 @@ CONFIG_PATH = 'automations.yaml' @asyncio.coroutine def async_setup(hass): """Set up the Automation config API.""" + async def hook(hass): + """post_write_hook for Config View that reloads automations.""" + await hass.services.async_call(DOMAIN, SERVICE_RELOAD) + hass.http.register_view(EditAutomationConfigView( DOMAIN, 'config', CONFIG_PATH, cv.string, - PLATFORM_SCHEMA, post_write_hook=async_reload + PLATFORM_SCHEMA, post_write_hook=hook )) return True diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index 408672a974f..af1bb1cd9b5 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -15,6 +15,7 @@ from homeassistant.setup import async_prepare_setup_platform from homeassistant.core import callback from homeassistant.loader import bind_hass from homeassistant.components import group, zone +from homeassistant.components.group import DOMAIN as DOMAIN_GROUP, SERVICE_SET from homeassistant.components.zone.zone import async_active_zone from homeassistant.config import load_yaml_config_file, async_log_exception from homeassistant.exceptions import HomeAssistantError @@ -319,9 +320,13 @@ class DeviceTracker: # During init, we ignore the group if self.group and self.track_new: - self.group.async_set_group( - util.slugify(GROUP_NAME_ALL_DEVICES), visible=False, - name=GROUP_NAME_ALL_DEVICES, add=[device.entity_id]) + self.hass.async_create_task( + self.hass.async_call( + DOMAIN_GROUP, SERVICE_SET, dict( + object_id=util.slugify(GROUP_NAME_ALL_DEVICES), + visible=False, + name=GROUP_NAME_ALL_DEVICES, + add=[device.entity_id]))) self.hass.bus.async_fire(EVENT_NEW_DEVICE, { ATTR_ENTITY_ID: device.entity_id, @@ -354,10 +359,13 @@ class DeviceTracker: entity_ids = [dev.entity_id for dev in self.devices.values() if dev.track] - self.group = self.hass.components.group - self.group.async_set_group( - util.slugify(GROUP_NAME_ALL_DEVICES), visible=False, - name=GROUP_NAME_ALL_DEVICES, entity_ids=entity_ids) + self.hass.async_create_task( + self.hass.services.async_call( + DOMAIN_GROUP, SERVICE_SET, dict( + object_id=util.slugify(GROUP_NAME_ALL_DEVICES), + visible=False, + name=GROUP_NAME_ALL_DEVICES, + entities=entity_ids))) @callback def async_update_stale(self, now: dt_util.dt.datetime): diff --git a/homeassistant/components/group/__init__.py b/homeassistant/components/group/__init__.py index 7b5a75dc13f..39fd7567c98 100644 --- a/homeassistant/components/group/__init__.py +++ b/homeassistant/components/group/__init__.py @@ -120,63 +120,6 @@ def is_on(hass, entity_id): return False -@bind_hass -def reload(hass): - """Reload the automation from config.""" - hass.add_job(async_reload, hass) - - -@callback -@bind_hass -def async_reload(hass): - """Reload the automation from config.""" - hass.async_add_job(hass.services.async_call(DOMAIN, SERVICE_RELOAD)) - - -@bind_hass -def set_group(hass, object_id, name=None, entity_ids=None, visible=None, - icon=None, view=None, control=None, add=None): - """Create/Update a group.""" - hass.add_job( - async_set_group, hass, object_id, name, entity_ids, visible, icon, - view, control, add) - - -@callback -@bind_hass -def async_set_group(hass, object_id, name=None, entity_ids=None, visible=None, - icon=None, view=None, control=None, add=None): - """Create/Update a group.""" - data = { - key: value for key, value in [ - (ATTR_OBJECT_ID, object_id), - (ATTR_NAME, name), - (ATTR_ENTITIES, entity_ids), - (ATTR_VISIBLE, visible), - (ATTR_ICON, icon), - (ATTR_VIEW, view), - (ATTR_CONTROL, control), - (ATTR_ADD_ENTITIES, add), - ] if value is not None - } - - hass.async_add_job(hass.services.async_call(DOMAIN, SERVICE_SET, data)) - - -@bind_hass -def remove(hass, name): - """Remove a user group.""" - hass.add_job(async_remove, hass, name) - - -@callback -@bind_hass -def async_remove(hass, object_id): - """Remove a user group.""" - data = {ATTR_OBJECT_ID: object_id} - hass.async_add_job(hass.services.async_call(DOMAIN, SERVICE_REMOVE, data)) - - @bind_hass def expand_entity_ids(hass, entity_ids): """Return entity_ids with group entity ids replaced by their members. diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py index 09f8838b160..c2ab8722c97 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -190,10 +190,13 @@ class EntityComponent: sorted(self.entities, key=lambda entity: entity.name or entity.entity_id)] - self.hass.components.group.async_set_group( - slugify(self.group_name), name=self.group_name, - visible=False, entity_ids=ids - ) + self.hass.async_create_task( + self.hass.services.async_call( + 'group', 'set', dict( + object_id=slugify(self.group_name), + name=self.group_name, + visible=False, + entities=ids))) async def _async_reset(self): """Remove entities and reset the entity component to initial values. @@ -212,7 +215,9 @@ class EntityComponent: self.config = None if self.group_name is not None: - self.hass.components.group.async_remove(slugify(self.group_name)) + await self.hass.services.async_call( + 'group', 'remove', dict( + object_id=slugify(self.group_name))) async def async_remove_entity(self, entity_id): """Remove an entity managed by one of the platforms.""" diff --git a/tests/components/group/common.py b/tests/components/group/common.py index 1a59eb265e0..380586e3854 100644 --- a/tests/components/group/common.py +++ b/tests/components/group/common.py @@ -3,12 +3,66 @@ All containing methods are legacy helpers that should not be used by new components. Instead call the service directly. """ -from homeassistant.components.group import ATTR_VISIBLE, DOMAIN, \ - SERVICE_SET_VISIBILITY -from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.components.group import ( + ATTR_ADD_ENTITIES, ATTR_CONTROL, ATTR_ENTITIES, ATTR_OBJECT_ID, ATTR_VIEW, + ATTR_VISIBLE, DOMAIN, SERVICE_REMOVE, SERVICE_SET, SERVICE_SET_VISIBILITY) +from homeassistant.const import ( + ATTR_ENTITY_ID, ATTR_ICON, ATTR_NAME, SERVICE_RELOAD) +from homeassistant.core import callback from homeassistant.loader import bind_hass +@bind_hass +def reload(hass): + """Reload the automation from config.""" + hass.add_job(async_reload, hass) + + +@callback +@bind_hass +def async_reload(hass): + """Reload the automation from config.""" + hass.async_add_job(hass.services.async_call(DOMAIN, SERVICE_RELOAD)) + + +@bind_hass +def set_group(hass, object_id, name=None, entity_ids=None, visible=None, + icon=None, view=None, control=None, add=None): + """Create/Update a group.""" + hass.add_job( + async_set_group, hass, object_id, name, entity_ids, visible, icon, + view, control, add) + + +@callback +@bind_hass +def async_set_group(hass, object_id, name=None, entity_ids=None, visible=None, + icon=None, view=None, control=None, add=None): + """Create/Update a group.""" + data = { + key: value for key, value in [ + (ATTR_OBJECT_ID, object_id), + (ATTR_NAME, name), + (ATTR_ENTITIES, entity_ids), + (ATTR_VISIBLE, visible), + (ATTR_ICON, icon), + (ATTR_VIEW, view), + (ATTR_CONTROL, control), + (ATTR_ADD_ENTITIES, add), + ] if value is not None + } + + hass.async_add_job(hass.services.async_call(DOMAIN, SERVICE_SET, data)) + + +@callback +@bind_hass +def async_remove(hass, object_id): + """Remove a user group.""" + data = {ATTR_OBJECT_ID: object_id} + hass.async_add_job(hass.services.async_call(DOMAIN, SERVICE_REMOVE, data)) + + @bind_hass def set_visibility(hass, entity_id=None, visible=True): """Hide or shows a group.""" diff --git a/tests/components/group/test_init.py b/tests/components/group/test_init.py index 619ce86583a..55c8a7778cb 100644 --- a/tests/components/group/test_init.py +++ b/tests/components/group/test_init.py @@ -368,7 +368,7 @@ class TestComponentsGroup(unittest.TestCase): }}}): with patch('homeassistant.config.find_config_file', return_value=''): - group.reload(self.hass) + common.reload(self.hass) self.hass.block_till_done() assert sorted(self.hass.states.entity_ids()) == \ @@ -409,7 +409,7 @@ class TestComponentsGroup(unittest.TestCase): # The old way would create a new group modify_group1 because # internally it didn't know anything about those created in the config - group.set_group(self.hass, 'modify_group', icon="mdi:play") + common.set_group(self.hass, 'modify_group', icon="mdi:play") self.hass.block_till_done() group_state = self.hass.states.get( @@ -442,7 +442,7 @@ def test_service_group_set_group_remove_group(hass): 'group': {} }) - group.async_set_group(hass, 'user_test_group', name="Test") + common.async_set_group(hass, 'user_test_group', name="Test") yield from hass.async_block_till_done() group_state = hass.states.get('group.user_test_group') @@ -450,7 +450,7 @@ def test_service_group_set_group_remove_group(hass): assert group_state.attributes[group.ATTR_AUTO] assert group_state.attributes['friendly_name'] == "Test" - group.async_set_group( + common.async_set_group( hass, 'user_test_group', view=True, visible=False, entity_ids=['test.entity_bla1']) yield from hass.async_block_till_done() @@ -463,7 +463,7 @@ def test_service_group_set_group_remove_group(hass): assert group_state.attributes['friendly_name'] == "Test" assert list(group_state.attributes['entity_id']) == ['test.entity_bla1'] - group.async_set_group( + common.async_set_group( hass, 'user_test_group', icon="mdi:camera", name="Test2", control="hidden", add=['test.entity_id2']) yield from hass.async_block_till_done() @@ -479,7 +479,7 @@ def test_service_group_set_group_remove_group(hass): assert sorted(list(group_state.attributes['entity_id'])) == sorted([ 'test.entity_bla1', 'test.entity_id2']) - group.async_remove(hass, 'user_test_group') + common.async_remove(hass, 'user_test_group') yield from hass.async_block_till_done() group_state = hass.states.get('group.user_test_group') From 9abdbf3db62ac07cf718910179f6741e1ee228b4 Mon Sep 17 00:00:00 2001 From: Greg Laabs Date: Thu, 27 Sep 2018 14:17:15 -0700 Subject: [PATCH 096/247] Rachio component modernization (#16911) Add `unique_id` to all rachio entities Add platform discovery to rachio component Move config options from switch.rachio platform to the rachio component --- .../components/binary_sensor/rachio.py | 5 ++++ homeassistant/components/rachio.py | 20 +++++++++++-- homeassistant/components/switch/rachio.py | 30 ++++++++++--------- 3 files changed, 38 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/binary_sensor/rachio.py b/homeassistant/components/binary_sensor/rachio.py index 798b6a754d1..36a32c79c5c 100644 --- a/homeassistant/components/binary_sensor/rachio.py +++ b/homeassistant/components/binary_sensor/rachio.py @@ -92,6 +92,11 @@ class RachioControllerOnlineBinarySensor(RachioControllerBinarySensor): """Return the name of this sensor including the controller name.""" return "{} online".format(self._controller.name) + @property + def unique_id(self) -> str: + """Return a unique id for this entity.""" + return "{}-online".format(self._controller.controller_id) + @property def device_class(self) -> str: """Return the class of this device, from component DEVICE_CLASSES.""" diff --git a/homeassistant/components/rachio.py b/homeassistant/components/rachio.py index cd80b7bec9b..27827da0182 100644 --- a/homeassistant/components/rachio.py +++ b/homeassistant/components/rachio.py @@ -13,7 +13,7 @@ import voluptuous as vol from homeassistant.auth.util import generate_secret from homeassistant.components.http import HomeAssistantView from homeassistant.const import CONF_API_KEY, EVENT_HOMEASSISTANT_STOP, URL_API -import homeassistant.helpers.config_validation as cv +from homeassistant.helpers import discovery, config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send REQUIREMENTS = ['rachiopy==0.1.3'] @@ -22,11 +22,19 @@ _LOGGER = logging.getLogger(__name__) DOMAIN = 'rachio' +SUPPORTED_DOMAINS = ['switch', 'binary_sensor'] + +# Manual run length +CONF_MANUAL_RUN_MINS = 'manual_run_mins' +DEFAULT_MANUAL_RUN_MINS = 10 CONF_CUSTOM_URL = 'hass_url_override' + CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ vol.Required(CONF_API_KEY): cv.string, vol.Optional(CONF_CUSTOM_URL): cv.string, + vol.Optional(CONF_MANUAL_RUN_MINS, default=DEFAULT_MANUAL_RUN_MINS): + cv.positive_int, }) }, extra=vol.ALLOW_EXTRA) @@ -112,7 +120,7 @@ def setup(hass, config) -> bool: # Get the API user try: - person = RachioPerson(hass, rachio) + person = RachioPerson(hass, rachio, config[DOMAIN]) except AssertionError as error: _LOGGER.error("Could not reach the Rachio API: %s", error) return False @@ -126,17 +134,23 @@ def setup(hass, config) -> bool: # Enable component hass.data[DOMAIN] = person + + # Load platforms + for component in SUPPORTED_DOMAINS: + discovery.load_platform(hass, component, DOMAIN, {}, config) + return True class RachioPerson: """Represent a Rachio user.""" - def __init__(self, hass, rachio): + def __init__(self, hass, rachio, config): """Create an object from the provided API instance.""" # Use API token to get user ID self._hass = hass self.rachio = rachio + self.config = config response = rachio.person.getInfo() assert int(response[0][KEY_STATUS]) == 200, "API key error" diff --git a/homeassistant/components/switch/rachio.py b/homeassistant/components/switch/rachio.py index 956befeeb9f..4797aae9a8c 100644 --- a/homeassistant/components/switch/rachio.py +++ b/homeassistant/components/switch/rachio.py @@ -7,10 +7,10 @@ https://home-assistant.io/components/switch.rachio/ from abc import abstractmethod from datetime import timedelta import logging -import voluptuous as vol -from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice -from homeassistant.components.rachio import (DOMAIN as DOMAIN_RACHIO, +from homeassistant.components.switch import SwitchDevice +from homeassistant.components.rachio import (CONF_MANUAL_RUN_MINS, + DOMAIN as DOMAIN_RACHIO, KEY_DEVICE_ID, KEY_ENABLED, KEY_ID, @@ -27,29 +27,20 @@ from homeassistant.components.rachio import (DOMAIN as DOMAIN_RACHIO, SUBTYPE_ZONE_COMPLETED, SUBTYPE_SLEEP_MODE_ON, SUBTYPE_SLEEP_MODE_OFF) -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import dispatcher_connect DEPENDENCIES = ['rachio'] _LOGGER = logging.getLogger(__name__) -# Manual run length -CONF_MANUAL_RUN_MINS = 'manual_run_mins' -DEFAULT_MANUAL_RUN_MINS = 10 - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_MANUAL_RUN_MINS, default=DEFAULT_MANUAL_RUN_MINS): - cv.positive_int, -}) - ATTR_ZONE_SUMMARY = 'Summary' ATTR_ZONE_NUMBER = 'Zone number' def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Rachio switches.""" - manual_run_time = timedelta(minutes=config.get(CONF_MANUAL_RUN_MINS)) + manual_run_time = timedelta(minutes=hass.data[DOMAIN_RACHIO].config.get( + CONF_MANUAL_RUN_MINS)) _LOGGER.info("Rachio run time is %s", str(manual_run_time)) # Add all zones from all controllers as switches @@ -126,6 +117,11 @@ class RachioStandbySwitch(RachioSwitch): """Return the name of the standby switch.""" return "{} in standby mode".format(self._controller.name) + @property + def unique_id(self) -> str: + """Return a unique id by combinining controller id and purpose.""" + return "{}-standby".format(self._controller.controller_id) + @property def icon(self) -> str: """Return an icon for the standby switch.""" @@ -189,6 +185,12 @@ class RachioZone(RachioSwitch): """Return the friendly name of the zone.""" return self._zone_name + @property + def unique_id(self) -> str: + """Return a unique id by combinining controller id and zone number.""" + return "{}-zone-{}".format(self._controller.controller_id, + self.zone_id) + @property def icon(self) -> str: """Return the icon to display.""" From c9b65672654dbdc57971b878feaec68ce86e9177 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Fri, 28 Sep 2018 00:25:51 +0200 Subject: [PATCH 097/247] Remove discovered mqtt_json light entity when discovery is cleared (#16906) * Remove discovered mqtt_json entity device when discovery topic is cleared * Keep imports ordered --- homeassistant/components/light/mqtt_json.py | 49 +++++++++++++-------- tests/components/light/test_mqtt_json.py | 25 ++++++++++- 2 files changed, 54 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/light/mqtt_json.py b/homeassistant/components/light/mqtt_json.py index ed4d350d96d..1ed43a6385a 100644 --- a/homeassistant/components/light/mqtt_json.py +++ b/homeassistant/components/light/mqtt_json.py @@ -4,29 +4,31 @@ Support for MQTT JSON lights. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/light.mqtt_json/ """ -import logging import json +import logging +from typing import Optional + import voluptuous as vol -from homeassistant.core import callback from homeassistant.components import mqtt from homeassistant.components.light import ( - ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_FLASH, - ATTR_TRANSITION, ATTR_WHITE_VALUE, ATTR_HS_COLOR, - FLASH_LONG, FLASH_SHORT, Light, PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, - SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, SUPPORT_FLASH, SUPPORT_COLOR, - SUPPORT_TRANSITION, SUPPORT_WHITE_VALUE) + ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_FLASH, ATTR_HS_COLOR, + ATTR_TRANSITION, ATTR_WHITE_VALUE, FLASH_LONG, FLASH_SHORT, + PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP, + SUPPORT_EFFECT, SUPPORT_FLASH, SUPPORT_TRANSITION, SUPPORT_WHITE_VALUE, + Light) from homeassistant.components.light.mqtt import CONF_BRIGHTNESS_SCALE -from homeassistant.const import ( - CONF_BRIGHTNESS, CONF_COLOR_TEMP, CONF_EFFECT, STATE_ON, - CONF_NAME, CONF_OPTIMISTIC, CONF_RGB, CONF_WHITE_VALUE, CONF_XY) from homeassistant.components.mqtt import ( - CONF_AVAILABILITY_TOPIC, CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, + ATTR_DISCOVERY_HASH, CONF_AVAILABILITY_TOPIC, CONF_COMMAND_TOPIC, CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, CONF_RETAIN, - MqttAvailability) + CONF_STATE_TOPIC, MqttAvailability, MqttDiscoveryUpdate) +from homeassistant.const import ( + CONF_BRIGHTNESS, CONF_COLOR_TEMP, CONF_EFFECT, CONF_NAME, CONF_OPTIMISTIC, + CONF_RGB, CONF_WHITE_VALUE, CONF_XY, STATE_ON) +from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.typing import HomeAssistantType, ConfigType from homeassistant.helpers.restore_state import async_get_last_state +from homeassistant.helpers.typing import ConfigType, HomeAssistantType import homeassistant.util.color as color_util _LOGGER = logging.getLogger(__name__) @@ -87,6 +89,11 @@ async def async_setup_platform(hass: HomeAssistantType, config: ConfigType, """Set up a MQTT JSON Light.""" if discovery_info is not None: config = PLATFORM_SCHEMA(discovery_info) + + discovery_hash = None + if discovery_info is not None and ATTR_DISCOVERY_HASH in discovery_info: + discovery_hash = discovery_info[ATTR_DISCOVERY_HASH] + async_add_entities([MqttJson( config.get(CONF_NAME), config.get(CONF_UNIQUE_ID), @@ -116,20 +123,23 @@ async def async_setup_platform(hass: HomeAssistantType, config: ConfigType, config.get(CONF_AVAILABILITY_TOPIC), config.get(CONF_PAYLOAD_AVAILABLE), config.get(CONF_PAYLOAD_NOT_AVAILABLE), - config.get(CONF_BRIGHTNESS_SCALE) + config.get(CONF_BRIGHTNESS_SCALE), + discovery_hash, )]) -class MqttJson(MqttAvailability, Light): +class MqttJson(MqttAvailability, MqttDiscoveryUpdate, Light): """Representation of a MQTT JSON light.""" def __init__(self, name, unique_id, effect_list, topic, qos, retain, optimistic, brightness, color_temp, effect, rgb, white_value, xy, hs, flash_times, availability_topic, payload_available, - payload_not_available, brightness_scale): + payload_not_available, brightness_scale, + discovery_hash: Optional[str]): """Initialize MQTT JSON light.""" - super().__init__(availability_topic, qos, payload_available, - payload_not_available) + MqttAvailability.__init__(self, availability_topic, qos, + payload_available, payload_not_available) + MqttDiscoveryUpdate.__init__(self, discovery_hash) self._name = name self._unique_id = unique_id self._effect_list = effect_list @@ -180,7 +190,8 @@ class MqttJson(MqttAvailability, Light): async def async_added_to_hass(self): """Subscribe to MQTT events.""" - await super().async_added_to_hass() + await MqttAvailability.async_added_to_hass(self) + await MqttDiscoveryUpdate.async_added_to_hass(self) last_state = await async_get_last_state(self.hass, self.entity_id) diff --git a/tests/components/light/test_mqtt_json.py b/tests/components/light/test_mqtt_json.py index dbae4c53bfc..46db2f61fb3 100644 --- a/tests/components/light/test_mqtt_json.py +++ b/tests/components/light/test_mqtt_json.py @@ -97,11 +97,12 @@ from homeassistant.const import ( STATE_ON, STATE_OFF, STATE_UNAVAILABLE, ATTR_ASSUMED_STATE, ATTR_SUPPORTED_FEATURES) import homeassistant.components.light as light +from homeassistant.components.mqtt.discovery import async_start import homeassistant.core as ha from tests.common import ( get_test_home_assistant, mock_mqtt_component, fire_mqtt_message, - assert_setup_component, mock_coro) + assert_setup_component, mock_coro, async_fire_mqtt_message) from tests.components.light import common @@ -669,3 +670,25 @@ class TestLightMQTTJSON(unittest.TestCase): state = self.hass.states.get('light.test') self.assertEqual(STATE_UNAVAILABLE, state.state) + + +async def test_discovery_removal(hass, mqtt_mock, caplog): + """Test removal of discovered mqtt_json lights.""" + await async_start(hass, 'homeassistant', {}) + data = ( + '{ "name": "Beer",' + ' "platform": "mqtt_json",' + ' "command_topic": "test_topic" }' + ) + async_fire_mqtt_message(hass, 'homeassistant/light/bla/config', + data) + await hass.async_block_till_done() + state = hass.states.get('light.beer') + assert state is not None + assert state.name == 'Beer' + async_fire_mqtt_message(hass, 'homeassistant/light/bla/config', + '') + await hass.async_block_till_done() + await hass.async_block_till_done() + state = hass.states.get('light.beer') + assert state is None From 720b05c301f1a22128839b71e8589b26623c33a1 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Fri, 28 Sep 2018 06:08:09 +0200 Subject: [PATCH 098/247] Fix race between script delay and turn_off (#16923) --- homeassistant/helpers/script.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index 96f9b2d5069..ac53a3e32a2 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -95,7 +95,7 @@ class Script(): def async_script_delay(now): """Handle delay.""" # pylint: disable=cell-var-from-loop - self._async_listener.remove(unsub) + self._async_remove_listener() self.hass.async_create_task( self.async_run(variables, context)) @@ -240,7 +240,7 @@ class Script(): @callback def async_script_timeout(now): """Call after timeout is retrieve.""" - self._async_listener.remove(unsub) + self._async_remove_listener() # Check if we want to continue to execute # the script after the timeout From af89e7c50f65afb233341525eb81d6dcc6ba599f Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Fri, 28 Sep 2018 16:57:17 +0200 Subject: [PATCH 099/247] Move more MQTT platforms to config entries (#16918) * Move more MQTT platforms to config entries * Address comments --- .../components/alarm_control_panel/mqtt.py | 34 ++++++++++++----- .../components/binary_sensor/mqtt.py | 36 ++++++++++++------ homeassistant/components/camera/mqtt.py | 30 +++++++++++---- homeassistant/components/climate/mqtt.py | 36 ++++++++++++------ homeassistant/components/cover/mqtt.py | 37 +++++++++++++------ homeassistant/components/light/mqtt.py | 35 +++++++++++++----- homeassistant/components/mqtt/discovery.py | 7 ++++ homeassistant/components/sensor/mqtt.py | 21 ++++------- homeassistant/components/switch/mqtt.py | 36 +++++++++++++----- .../alarm_control_panel/test_mqtt.py | 7 ++-- tests/components/binary_sensor/test_mqtt.py | 8 ++-- tests/components/climate/test_mqtt.py | 7 ++-- tests/components/cover/test_mqtt.py | 7 ++-- tests/components/light/test_mqtt.py | 7 ++-- tests/components/switch/test_mqtt.py | 7 ++-- 15 files changed, 214 insertions(+), 101 deletions(-) diff --git a/homeassistant/components/alarm_control_panel/mqtt.py b/homeassistant/components/alarm_control_panel/mqtt.py index e36765b2460..d8193f958da 100644 --- a/homeassistant/components/alarm_control_panel/mqtt.py +++ b/homeassistant/components/alarm_control_panel/mqtt.py @@ -21,7 +21,10 @@ 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) +from homeassistant.components.mqtt.discovery import MQTT_DISCOVERY_NEW import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.typing import HomeAssistantType, ConfigType _LOGGER = logging.getLogger(__name__) @@ -46,17 +49,28 @@ PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({ }).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass: HomeAssistantType, config: ConfigType, + async_add_entities, discovery_info=None): + """Set up MQTT alarm control panel through configuration.yaml.""" + await _async_setup_entity(hass, config, async_add_entities) + + +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(hass, config, async_add_entities, + discovery_payload[ATTR_DISCOVERY_HASH]) + + async_dispatcher_connect( + hass, MQTT_DISCOVERY_NEW.format(alarm.DOMAIN, 'mqtt'), + async_discover) + + +async def _async_setup_entity(hass, config, async_add_entities, + discovery_hash=None): """Set up the MQTT Alarm Control Panel platform.""" - if discovery_info is not None: - config = PLATFORM_SCHEMA(discovery_info) - - discovery_hash = None - if discovery_info is not None and ATTR_DISCOVERY_HASH in discovery_info: - discovery_hash = discovery_info[ATTR_DISCOVERY_HASH] - async_add_entities([MqttAlarm( config.get(CONF_NAME), config.get(CONF_STATE_TOPIC), diff --git a/homeassistant/components/binary_sensor/mqtt.py b/homeassistant/components/binary_sensor/mqtt.py index ca2ff2074c3..944cea96b33 100644 --- a/homeassistant/components/binary_sensor/mqtt.py +++ b/homeassistant/components/binary_sensor/mqtt.py @@ -11,7 +11,7 @@ from typing import Optional import voluptuous as vol from homeassistant.core import callback -from homeassistant.components import mqtt +from homeassistant.components import mqtt, binary_sensor from homeassistant.components.binary_sensor import ( BinarySensorDevice, DEVICE_CLASSES_SCHEMA) from homeassistant.const import ( @@ -21,7 +21,10 @@ from homeassistant.components.mqtt import ( ATTR_DISCOVERY_HASH, CONF_STATE_TOPIC, CONF_AVAILABILITY_TOPIC, CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, MqttAvailability, MqttDiscoveryUpdate) +from homeassistant.components.mqtt.discovery import MQTT_DISCOVERY_NEW import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.typing import HomeAssistantType, ConfigType _LOGGER = logging.getLogger(__name__) @@ -45,21 +48,32 @@ PLATFORM_SCHEMA = mqtt.MQTT_RO_PLATFORM_SCHEMA.extend({ }).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): - """Set up the MQTT binary sensor.""" - if discovery_info is not None: - config = PLATFORM_SCHEMA(discovery_info) +async def async_setup_platform(hass: HomeAssistantType, config: ConfigType, + async_add_entities, discovery_info=None): + """Set up MQTT binary sensor through configuration.yaml.""" + await _async_setup_entity(hass, config, async_add_entities) + +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(hass, config, async_add_entities, + discovery_payload[ATTR_DISCOVERY_HASH]) + + async_dispatcher_connect( + hass, MQTT_DISCOVERY_NEW.format(binary_sensor.DOMAIN, 'mqtt'), + async_discover) + + +async def _async_setup_entity(hass, config, async_add_entities, + discovery_hash=None): + """Set up the MQTT binary sensor.""" value_template = config.get(CONF_VALUE_TEMPLATE) if value_template is not None: value_template.hass = hass - discovery_hash = None - if discovery_info is not None and ATTR_DISCOVERY_HASH in discovery_info: - discovery_hash = discovery_info[ATTR_DISCOVERY_HASH] - async_add_entities([MqttBinarySensor( config.get(CONF_NAME), config.get(CONF_STATE_TOPIC), diff --git a/homeassistant/components/camera/mqtt.py b/homeassistant/components/camera/mqtt.py index 13c1745615d..42ad7d6fa66 100644 --- a/homeassistant/components/camera/mqtt.py +++ b/homeassistant/components/camera/mqtt.py @@ -10,10 +10,13 @@ import logging import voluptuous as vol +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.typing import HomeAssistantType, ConfigType from homeassistant.core import callback -from homeassistant.components import mqtt from homeassistant.const import CONF_NAME +from homeassistant.components import mqtt, camera from homeassistant.components.camera import Camera, PLATFORM_SCHEMA +from homeassistant.components.mqtt.discovery import MQTT_DISCOVERY_NEW from homeassistant.helpers import config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -31,13 +34,26 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): - """Set up the MQTT Camera.""" - if discovery_info is not None: - config = PLATFORM_SCHEMA(discovery_info) +async def async_setup_platform(hass: HomeAssistantType, config: ConfigType, + async_add_entities, discovery_info=None): + """Set up MQTT camera through configuration.yaml.""" + await _async_setup_entity(hass, config, async_add_entities) + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up MQTT camera dynamically through MQTT discovery.""" + async def async_discover(discovery_payload): + """Discover and add a MQTT camera.""" + config = PLATFORM_SCHEMA(discovery_payload) + await _async_setup_entity(hass, config, async_add_entities) + + async_dispatcher_connect( + hass, MQTT_DISCOVERY_NEW.format(camera.DOMAIN, 'mqtt'), + async_discover) + + +async def _async_setup_entity(hass, config, async_add_entities): + """Set up the MQTT Camera.""" async_add_entities([MqttCamera( config.get(CONF_NAME), config.get(CONF_UNIQUE_ID), diff --git a/homeassistant/components/climate/mqtt.py b/homeassistant/components/climate/mqtt.py index 66f76ac1aaa..23def7c4b87 100644 --- a/homeassistant/components/climate/mqtt.py +++ b/homeassistant/components/climate/mqtt.py @@ -10,7 +10,7 @@ import logging import voluptuous as vol from homeassistant.core import callback -from homeassistant.components import mqtt +from homeassistant.components import mqtt, climate from homeassistant.components.climate import ( STATE_HEAT, STATE_COOL, STATE_DRY, STATE_FAN_ONLY, ClimateDevice, @@ -24,7 +24,10 @@ 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) +from homeassistant.components.mqtt.discovery import MQTT_DISCOVERY_NEW import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.typing import HomeAssistantType, ConfigType from homeassistant.components.fan import (SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH) @@ -127,13 +130,28 @@ PLATFORM_SCHEMA = SCHEMA_BASE.extend({ }).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): - """Set up the MQTT climate devices.""" - if discovery_info is not None: - config = PLATFORM_SCHEMA(discovery_info) +async def async_setup_platform(hass: HomeAssistantType, config: ConfigType, + async_add_entities, discovery_info=None): + """Set up MQTT climate device through configuration.yaml.""" + await _async_setup_entity(hass, config, async_add_entities) + +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]) + + async_dispatcher_connect( + hass, MQTT_DISCOVERY_NEW.format(climate.DOMAIN, 'mqtt'), + async_discover) + + +async def _async_setup_entity(hass, config, async_add_entities, + discovery_hash=None): + """Set up the MQTT climate devices.""" template_keys = ( CONF_POWER_STATE_TEMPLATE, CONF_MODE_STATE_TEMPLATE, @@ -154,10 +172,6 @@ def async_setup_platform(hass, config, async_add_entities, value_templates[key] = config.get(key) value_templates[key].hass = hass - discovery_hash = None - if discovery_info is not None and ATTR_DISCOVERY_HASH in discovery_info: - discovery_hash = discovery_info[ATTR_DISCOVERY_HASH] - async_add_entities([ MqttClimate( hass, diff --git a/homeassistant/components/cover/mqtt.py b/homeassistant/components/cover/mqtt.py index ae1162f7120..a239f4cd4f8 100644 --- a/homeassistant/components/cover/mqtt.py +++ b/homeassistant/components/cover/mqtt.py @@ -9,7 +9,7 @@ import logging import voluptuous as vol from homeassistant.core import callback -from homeassistant.components import mqtt +from homeassistant.components import mqtt, cover from homeassistant.components.cover import ( CoverDevice, ATTR_TILT_POSITION, SUPPORT_OPEN_TILT, SUPPORT_CLOSE_TILT, SUPPORT_STOP_TILT, SUPPORT_SET_TILT_POSITION, @@ -24,7 +24,10 @@ 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) +from homeassistant.components.mqtt.discovery import MQTT_DISCOVERY_NEW import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.typing import HomeAssistantType, ConfigType _LOGGER = logging.getLogger(__name__) @@ -93,12 +96,28 @@ PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({ }).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): - """Set up the MQTT Cover.""" - if discovery_info is not None: - config = PLATFORM_SCHEMA(discovery_info) +async def async_setup_platform(hass: HomeAssistantType, config: ConfigType, + async_add_entities, discovery_info=None): + """Set up MQTT cover through configuration.yaml.""" + await _async_setup_entity(hass, config, async_add_entities) + +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(hass, config, async_add_entities, + discovery_payload[ATTR_DISCOVERY_HASH]) + + async_dispatcher_connect( + hass, MQTT_DISCOVERY_NEW.format(cover.DOMAIN, 'mqtt'), + async_discover) + + +async def _async_setup_entity(hass, config, async_add_entities, + discovery_hash=None): + """Set up the MQTT Cover.""" value_template = config.get(CONF_VALUE_TEMPLATE) if value_template is not None: value_template.hass = hass @@ -106,10 +125,6 @@ async def async_setup_platform(hass, config, async_add_entities, if set_position_template is not None: set_position_template.hass = hass - discovery_hash = None - if discovery_info is not None and ATTR_DISCOVERY_HASH in discovery_info: - discovery_hash = discovery_info[ATTR_DISCOVERY_HASH] - async_add_entities([MqttCover( config.get(CONF_NAME), config.get(CONF_STATE_TOPIC), @@ -136,7 +151,7 @@ async def async_setup_platform(hass, config, async_add_entities, config.get(CONF_TILT_INVERT_STATE), config.get(CONF_POSITION_TOPIC), set_position_template, - discovery_hash, + discovery_hash )]) diff --git a/homeassistant/components/light/mqtt.py b/homeassistant/components/light/mqtt.py index 8bd50700c53..3b095aa4bfd 100644 --- a/homeassistant/components/light/mqtt.py +++ b/homeassistant/components/light/mqtt.py @@ -9,7 +9,7 @@ import logging import voluptuous as vol from homeassistant.core import callback -from homeassistant.components import mqtt +from homeassistant.components import mqtt, light from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_HS_COLOR, ATTR_WHITE_VALUE, Light, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, @@ -22,7 +22,10 @@ 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) +from homeassistant.components.mqtt.discovery import MQTT_DISCOVERY_NEW from homeassistant.helpers.restore_state import async_get_last_state +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.typing import HomeAssistantType, ConfigType import homeassistant.helpers.config_validation as cv import homeassistant.util.color as color_util @@ -102,19 +105,31 @@ PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({ }).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): - """Set up a MQTT Light.""" - if discovery_info is not None: - config = PLATFORM_SCHEMA(discovery_info) +async def async_setup_platform(hass: HomeAssistantType, config: ConfigType, + async_add_entities, discovery_info=None): + """Set up MQTT light through configuration.yaml.""" + await _async_setup_entity(hass, config, async_add_entities) + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up MQTT light dynamically through MQTT discovery.""" + async def async_discover(discovery_payload): + """Discover and add a MQTT light.""" + config = PLATFORM_SCHEMA(discovery_payload) + await _async_setup_entity(hass, config, async_add_entities, + discovery_payload[ATTR_DISCOVERY_HASH]) + + async_dispatcher_connect( + hass, MQTT_DISCOVERY_NEW.format(light.DOMAIN, 'mqtt'), + async_discover) + + +async def _async_setup_entity(hass, config, async_add_entities, + discovery_hash=None): + """Set up a MQTT Light.""" config.setdefault( CONF_STATE_VALUE_TEMPLATE, config.get(CONF_VALUE_TEMPLATE)) - discovery_hash = None - if discovery_info is not None and ATTR_DISCOVERY_HASH in discovery_info: - discovery_hash = discovery_info[ATTR_DISCOVERY_HASH] - async_add_entities([MqttLight( config.get(CONF_NAME), config.get(CONF_UNIQUE_ID), diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py index d5ed5e25b47..6a0b8555ddb 100644 --- a/homeassistant/components/mqtt/discovery.py +++ b/homeassistant/components/mqtt/discovery.py @@ -40,7 +40,14 @@ ALLOWED_PLATFORMS = { } CONFIG_ENTRY_PLATFORMS = { + 'binary_sensor': ['mqtt'], + 'camera': ['mqtt'], + 'cover': ['mqtt'], + 'light': ['mqtt'], 'sensor': ['mqtt'], + 'switch': ['mqtt'], + 'climate': ['mqtt'], + 'alarm_control_panel': ['mqtt'], } ALREADY_DISCOVERED = 'mqtt_discovered_components' diff --git a/homeassistant/components/sensor/mqtt.py b/homeassistant/components/sensor/mqtt.py index 2d15fca755b..fe0b77b2024 100644 --- a/homeassistant/components/sensor/mqtt.py +++ b/homeassistant/components/sensor/mqtt.py @@ -57,34 +57,29 @@ PLATFORM_SCHEMA = mqtt.MQTT_RO_PLATFORM_SCHEMA.extend({ async def async_setup_platform(hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None): """Set up MQTT sensors through configuration.yaml.""" - await _async_setup_platform(hass, config, async_add_entities, - discovery_info) + await _async_setup_entity(hass, config, async_add_entities) async def async_setup_entry(hass, config_entry, async_add_entities): """Set up MQTT sensors dynamically through MQTT discovery.""" - async def async_discover_sensor(config): + async def async_discover_sensor(discovery_payload): """Discover and add a discovered MQTT sensor.""" - await _async_setup_platform(hass, {}, async_add_entities, config) + config = PLATFORM_SCHEMA(discovery_payload) + await _async_setup_entity(hass, config, async_add_entities, + discovery_payload[ATTR_DISCOVERY_HASH]) async_dispatcher_connect(hass, MQTT_DISCOVERY_NEW.format(sensor.DOMAIN, 'mqtt'), async_discover_sensor) -async def _async_setup_platform(hass: HomeAssistantType, config: ConfigType, - async_add_entities, discovery_info=None): - if discovery_info is not None: - config = PLATFORM_SCHEMA(discovery_info) - +async def _async_setup_entity(hass: HomeAssistantType, config: ConfigType, + async_add_entities, discovery_hash=None): + """Set up MQTT sensor.""" value_template = config.get(CONF_VALUE_TEMPLATE) if value_template is not None: value_template.hass = hass - discovery_hash = None - if discovery_info is not None and ATTR_DISCOVERY_HASH in discovery_info: - discovery_hash = discovery_info[ATTR_DISCOVERY_HASH] - async_add_entities([MqttSensor( config.get(CONF_NAME), config.get(CONF_STATE_TOPIC), diff --git a/homeassistant/components/switch/mqtt.py b/homeassistant/components/switch/mqtt.py index b79f8f12b87..bb57f179340 100644 --- a/homeassistant/components/switch/mqtt.py +++ b/homeassistant/components/switch/mqtt.py @@ -15,12 +15,15 @@ from homeassistant.components.mqtt import ( CONF_AVAILABILITY_TOPIC, CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, CONF_RETAIN, MqttAvailability, MqttDiscoveryUpdate) +from homeassistant.components.mqtt.discovery import MQTT_DISCOVERY_NEW from homeassistant.components.switch import SwitchDevice from homeassistant.const import ( CONF_NAME, CONF_OPTIMISTIC, CONF_VALUE_TEMPLATE, CONF_PAYLOAD_OFF, CONF_PAYLOAD_ON, CONF_ICON, STATE_ON) -from homeassistant.components import mqtt +from homeassistant.components import mqtt, switch import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.typing import HomeAssistantType, ConfigType from homeassistant.helpers.restore_state import async_get_last_state _LOGGER = logging.getLogger(__name__) @@ -47,20 +50,33 @@ PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({ }).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): - """Set up the MQTT switch.""" - if discovery_info is not None: - config = PLATFORM_SCHEMA(discovery_info) +async def async_setup_platform(hass: HomeAssistantType, config: ConfigType, + async_add_entities, discovery_info=None): + """Set up MQTT switch through configuration.yaml.""" + await _async_setup_entity(hass, config, async_add_entities, + discovery_info) + +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(hass, config, async_add_entities, + discovery_payload[ATTR_DISCOVERY_HASH]) + + async_dispatcher_connect( + hass, MQTT_DISCOVERY_NEW.format(switch.DOMAIN, 'mqtt'), + async_discover) + + +async def _async_setup_entity(hass, config, async_add_entities, + discovery_hash=None): + """Set up the MQTT switch.""" value_template = config.get(CONF_VALUE_TEMPLATE) if value_template is not None: value_template.hass = hass - discovery_hash = None - if discovery_info is not None and ATTR_DISCOVERY_HASH in discovery_info: - discovery_hash = discovery_info[ATTR_DISCOVERY_HASH] - newswitch = MqttSwitch( config.get(CONF_NAME), config.get(CONF_ICON), diff --git a/tests/components/alarm_control_panel/test_mqtt.py b/tests/components/alarm_control_panel/test_mqtt.py index b77767980a7..dd606bb53ec 100644 --- a/tests/components/alarm_control_panel/test_mqtt.py +++ b/tests/components/alarm_control_panel/test_mqtt.py @@ -6,12 +6,12 @@ from homeassistant.const import ( STATE_ALARM_DISARMED, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_AWAY, STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED, STATE_UNAVAILABLE, STATE_UNKNOWN) -from homeassistant.components import alarm_control_panel +from homeassistant.components import alarm_control_panel, mqtt from homeassistant.components.mqtt.discovery import async_start from tests.common import ( mock_mqtt_component, async_fire_mqtt_message, fire_mqtt_message, - get_test_home_assistant, assert_setup_component) + get_test_home_assistant, assert_setup_component, MockConfigEntry) from tests.components.alarm_control_panel import common CODE = 'HELLO_CODE' @@ -245,7 +245,8 @@ class TestAlarmControlPanelMQTT(unittest.TestCase): async def test_discovery_removal_alarm(hass, mqtt_mock, caplog): """Test removal of discovered alarm_control_panel.""" - await async_start(hass, 'homeassistant', {}) + entry = MockConfigEntry(domain=mqtt.DOMAIN) + await async_start(hass, 'homeassistant', {}, entry) data = ( '{ "name": "Beer",' diff --git a/tests/components/binary_sensor/test_mqtt.py b/tests/components/binary_sensor/test_mqtt.py index dfa01898ba4..84619ce4ee6 100644 --- a/tests/components/binary_sensor/test_mqtt.py +++ b/tests/components/binary_sensor/test_mqtt.py @@ -3,7 +3,7 @@ import unittest import homeassistant.core as ha from homeassistant.setup import setup_component, async_setup_component -import homeassistant.components.binary_sensor as binary_sensor +from homeassistant.components import binary_sensor, mqtt from homeassistant.components.mqtt.discovery import async_start from homeassistant.const import STATE_OFF, STATE_ON @@ -11,7 +11,8 @@ from homeassistant.const import EVENT_STATE_CHANGED, STATE_UNAVAILABLE from tests.common import ( get_test_home_assistant, fire_mqtt_message, async_fire_mqtt_message, - mock_component, mock_mqtt_component, async_mock_mqtt_component) + mock_component, mock_mqtt_component, async_mock_mqtt_component, + MockConfigEntry) class TestSensorMQTT(unittest.TestCase): @@ -231,7 +232,8 @@ async def test_unique_id(hass): async def test_discovery_removal_binary_sensor(hass, mqtt_mock, caplog): """Test removal of discovered binary_sensor.""" - await async_start(hass, 'homeassistant', {}) + entry = MockConfigEntry(domain=mqtt.DOMAIN) + await async_start(hass, 'homeassistant', {}, entry) data = ( '{ "name": "Beer",' ' "status_topic": "test_topic" }' diff --git a/tests/components/climate/test_mqtt.py b/tests/components/climate/test_mqtt.py index 1fdf72e34ba..c63dbf26690 100644 --- a/tests/components/climate/test_mqtt.py +++ b/tests/components/climate/test_mqtt.py @@ -6,7 +6,7 @@ from homeassistant.util.unit_system import ( METRIC_SYSTEM ) from homeassistant.setup import setup_component -from homeassistant.components import climate +from homeassistant.components import climate, mqtt from homeassistant.const import STATE_OFF, STATE_UNAVAILABLE from homeassistant.components.climate import ( SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, @@ -15,7 +15,7 @@ from homeassistant.components.climate import ( from homeassistant.components.mqtt.discovery import async_start from tests.common import (get_test_home_assistant, mock_mqtt_component, async_fire_mqtt_message, fire_mqtt_message, - mock_component) + mock_component, MockConfigEntry) from tests.components.climate import common ENTITY_CLIMATE = 'climate.test' @@ -656,7 +656,8 @@ class TestMQTTClimate(unittest.TestCase): async def test_discovery_removal_climate(hass, mqtt_mock, caplog): """Test removal of discovered climate.""" - await async_start(hass, 'homeassistant', {}) + entry = MockConfigEntry(domain=mqtt.DOMAIN) + await async_start(hass, 'homeassistant', {}, entry) data = ( '{ "name": "Beer" }' ) diff --git a/tests/components/cover/test_mqtt.py b/tests/components/cover/test_mqtt.py index f1d3da9be84..b41a047e3db 100644 --- a/tests/components/cover/test_mqtt.py +++ b/tests/components/cover/test_mqtt.py @@ -1,7 +1,7 @@ """The tests for the MQTT cover platform.""" import unittest -import homeassistant.components.cover as cover +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.discovery import async_start @@ -15,7 +15,7 @@ from homeassistant.setup import setup_component from tests.common import ( get_test_home_assistant, mock_mqtt_component, async_fire_mqtt_message, - fire_mqtt_message) + fire_mqtt_message, MockConfigEntry) class TestCoverMQTT(unittest.TestCase): @@ -761,7 +761,8 @@ class TestCoverMQTT(unittest.TestCase): async def test_discovery_removal_cover(hass, mqtt_mock, caplog): """Test removal of discovered cover.""" - await async_start(hass, 'homeassistant', {}) + entry = MockConfigEntry(domain=mqtt.DOMAIN) + await async_start(hass, 'homeassistant', {}, entry) data = ( '{ "name": "Beer",' ' "command_topic": "test_topic" }' diff --git a/tests/components/light/test_mqtt.py b/tests/components/light/test_mqtt.py index b4c76b6b895..118cdb3c995 100644 --- a/tests/components/light/test_mqtt.py +++ b/tests/components/light/test_mqtt.py @@ -145,13 +145,13 @@ from unittest.mock import patch from homeassistant.setup import setup_component from homeassistant.const import ( STATE_ON, STATE_OFF, STATE_UNAVAILABLE, ATTR_ASSUMED_STATE) -import homeassistant.components.light as light +from homeassistant.components import light, mqtt from homeassistant.components.mqtt.discovery import async_start import homeassistant.core as ha from tests.common import ( assert_setup_component, get_test_home_assistant, mock_mqtt_component, - async_fire_mqtt_message, fire_mqtt_message, mock_coro) + async_fire_mqtt_message, fire_mqtt_message, mock_coro, MockConfigEntry) from tests.components.light import common @@ -883,7 +883,8 @@ class TestLightMQTT(unittest.TestCase): async def test_discovery_removal_light(hass, mqtt_mock, caplog): """Test removal of discovered light.""" - await async_start(hass, 'homeassistant', {}) + entry = MockConfigEntry(domain=mqtt.DOMAIN) + await async_start(hass, 'homeassistant', {}, entry) data = ( '{ "name": "Beer",' diff --git a/tests/components/switch/test_mqtt.py b/tests/components/switch/test_mqtt.py index ac113a05829..5ad233de284 100644 --- a/tests/components/switch/test_mqtt.py +++ b/tests/components/switch/test_mqtt.py @@ -6,12 +6,12 @@ from homeassistant.setup import setup_component, async_setup_component from homeassistant.const import STATE_ON, STATE_OFF, STATE_UNAVAILABLE,\ ATTR_ASSUMED_STATE import homeassistant.core as ha -import homeassistant.components.switch as switch +from homeassistant.components import switch, mqtt from homeassistant.components.mqtt.discovery import async_start from tests.common import ( mock_mqtt_component, fire_mqtt_message, get_test_home_assistant, mock_coro, - async_mock_mqtt_component, async_fire_mqtt_message) + async_mock_mqtt_component, async_fire_mqtt_message, MockConfigEntry) from tests.components.switch import common @@ -313,7 +313,8 @@ async def test_unique_id(hass): async def test_discovery_removal_switch(hass, mqtt_mock, caplog): """Test expansion of discovered switch.""" - await async_start(hass, 'homeassistant', {}) + entry = MockConfigEntry(domain=mqtt.DOMAIN) + await async_start(hass, 'homeassistant', {}, entry) data = ( '{ "name": "Beer",' From 7d1960baba0c9d6d5bcd86aaecdd9129f84ba9fc Mon Sep 17 00:00:00 2001 From: Jan van Helvoort Date: Fri, 28 Sep 2018 19:14:57 +0200 Subject: [PATCH 100/247] Add zwave.network_complete_some_dead event (#16894) * Add zwave.network_complete_some_dead event * add missing comma * typo * Add SIGNAL_AWAKE_NODES_QUERIED_SOME_DEAD Test * Add blank lines * fix linter warnings Line too long * remove trailing whitespace * Change test signal * Listen to other event --- homeassistant/components/zwave/__init__.py | 13 ++++++++++- homeassistant/components/zwave/const.py | 1 + tests/components/zwave/test_init.py | 27 ++++++++++++++++++++++ tests/mock/zwave.py | 3 ++- 4 files changed, 42 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zwave/__init__.py b/homeassistant/components/zwave/__init__.py index 8de8c713f04..fa78f719557 100644 --- a/homeassistant/components/zwave/__init__.py +++ b/homeassistant/components/zwave/__init__.py @@ -275,7 +275,9 @@ async def async_setup(hass, config): ZWaveNetwork.SIGNAL_SCENE_EVENT, ZWaveNetwork.SIGNAL_NODE_EVENT, ZWaveNetwork.SIGNAL_AWAKE_NODES_QUERIED, - ZWaveNetwork.SIGNAL_ALL_NODES_QUERIED): + ZWaveNetwork.SIGNAL_ALL_NODES_QUERIED, + ZWaveNetwork + .SIGNAL_ALL_NODES_QUERIED_SOME_DEAD): pprint(_obj_to_dict(value)) print("") @@ -356,6 +358,12 @@ async def async_setup(hass, config): "have been queried") hass.bus.fire(const.EVENT_NETWORK_COMPLETE) + def network_complete_some_dead(): + """Handle the querying of all nodes on network.""" + _LOGGER.info("Z-Wave network is complete. All nodes on the network " + "have been queried, but some node are marked dead") + hass.bus.fire(const.EVENT_NETWORK_COMPLETE_SOME_DEAD) + dispatcher.connect( value_added, ZWaveNetwork.SIGNAL_VALUE_ADDED, weak=False) dispatcher.connect( @@ -364,6 +372,9 @@ async def async_setup(hass, config): network_ready, ZWaveNetwork.SIGNAL_AWAKE_NODES_QUERIED, weak=False) dispatcher.connect( network_complete, ZWaveNetwork.SIGNAL_ALL_NODES_QUERIED, weak=False) + dispatcher.connect( + network_complete_some_dead, + ZWaveNetwork.SIGNAL_ALL_NODES_QUERIED_SOME_DEAD, weak=False) def add_node(service): """Switch into inclusion mode.""" diff --git a/homeassistant/components/zwave/const.py b/homeassistant/components/zwave/const.py index 775da8fbc51..b84f0287349 100644 --- a/homeassistant/components/zwave/const.py +++ b/homeassistant/components/zwave/const.py @@ -60,6 +60,7 @@ EVENT_SCENE_ACTIVATED = "zwave.scene_activated" EVENT_NODE_EVENT = "zwave.node_event" EVENT_NETWORK_READY = "zwave.network_ready" EVENT_NETWORK_COMPLETE = "zwave.network_complete" +EVENT_NETWORK_COMPLETE_SOME_DEAD = "zwave.network_complete_some_dead" EVENT_NETWORK_START = "zwave.network_start" EVENT_NETWORK_STOP = "zwave.network_stop" diff --git a/tests/components/zwave/test_init.py b/tests/components/zwave/test_init.py index 2b3019b2f8d..a2290d8aabf 100644 --- a/tests/components/zwave/test_init.py +++ b/tests/components/zwave/test_init.py @@ -458,6 +458,33 @@ def test_network_complete(hass, mock_openzwave): assert len(events) == 1 +@asyncio.coroutine +def test_network_complete_some_dead(hass, mock_openzwave): + """Test Node network complete some dead event.""" + mock_receivers = [] + + def mock_connect(receiver, signal, *args, **kwargs): + if signal == MockNetwork.SIGNAL_ALL_NODES_QUERIED_SOME_DEAD: + mock_receivers.append(receiver) + + with patch('pydispatch.dispatcher.connect', new=mock_connect): + yield from async_setup_component(hass, 'zwave', {'zwave': {}}) + + assert len(mock_receivers) == 1 + + events = [] + + def listener(event): + events.append(event) + + hass.bus.async_listen(const.EVENT_NETWORK_COMPLETE_SOME_DEAD, listener) + + hass.async_add_job(mock_receivers[0]) + yield from hass.async_block_till_done() + + assert len(events) == 1 + + class TestZWaveDeviceEntityValues(unittest.TestCase): """Tests for the ZWaveDeviceEntityValues helper.""" diff --git a/tests/mock/zwave.py b/tests/mock/zwave.py index 59d97ddb621..36735b1693b 100644 --- a/tests/mock/zwave.py +++ b/tests/mock/zwave.py @@ -88,7 +88,8 @@ class MockNetwork(MagicMock): SIGNAL_NODE_QUERIES_COMPLETE = 'mock_NodeQueriesComplete' SIGNAL_AWAKE_NODES_QUERIED = 'mock_AwakeNodesQueried' SIGNAL_ALL_NODES_QUERIED = 'mock_AllNodesQueried' - SIGNAL_ALL_NODES_QUERIED_SOME_DEAD = 'mock_AllNodesQueriedSomeDead' + SIGNAL_ALL_NODES_QUERIED_SOME_DEAD = \ + 'mock_AllNodesQueriedSomeDead' SIGNAL_MSG_COMPLETE = 'mock_MsgComplete' SIGNAL_NOTIFICATION = 'mock_Notification' SIGNAL_CONTROLLER_COMMAND = 'mock_ControllerCommand' From 4b8c38819e2d3e0816fdae9d501ea90ae3865fe5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ab=C3=ADlio=20Costa?= Date: Fri, 28 Sep 2018 22:47:29 +0100 Subject: [PATCH 101/247] Add myself to CODEOWNERS --- CODEOWNERS | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CODEOWNERS b/CODEOWNERS index b6ce8c04909..4b4019151b5 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -50,6 +50,7 @@ homeassistant/components/climate/sensibo.py @andrey-git homeassistant/components/cover/group.py @cdce8p homeassistant/components/cover/template.py @PhracturedBlue homeassistant/components/device_tracker/automatic.py @armills +homeassistant/components/device_tracker/huawei_router.py @abmantis homeassistant/components/device_tracker/tile.py @bachya homeassistant/components/history_graph.py @andrey-git homeassistant/components/light/lifx.py @amelchio @@ -93,6 +94,8 @@ homeassistant/components/*/broadlink.py @danielhiversen homeassistant/components/*/deconz.py @kane610 homeassistant/components/ecovacs.py @OverloadUT homeassistant/components/*/ecovacs.py @OverloadUT +homeassistant/components/edp_redy.py @abmantis +homeassistant/components/*/edp_redy.py @abmantis homeassistant/components/eight_sleep.py @mezz64 homeassistant/components/*/eight_sleep.py @mezz64 homeassistant/components/hive.py @Rendili @KJonline From c600d28b6a1b064cf6bfc80080572bb597661aff Mon Sep 17 00:00:00 2001 From: Jason Hu Date: Sat, 29 Sep 2018 11:53:02 -0700 Subject: [PATCH 102/247] Override unique_id of NestActivityZoneSensor (#16961) --- homeassistant/components/binary_sensor/nest.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/binary_sensor/nest.py b/homeassistant/components/binary_sensor/nest.py index c60463a8663..7f7278d9789 100644 --- a/homeassistant/components/binary_sensor/nest.py +++ b/homeassistant/components/binary_sensor/nest.py @@ -147,6 +147,11 @@ class NestActivityZoneSensor(NestBinarySensor): self.zone = zone self._name = "{} {} activity".format(self._name, self.zone.name) + @property + def unique_id(self): + """Return unique id based on camera serial and zone id.""" + return "{}-{}".format(self.device.serial, self.zone.zone_id) + @property def device_class(self): """Return the device class of the binary sensor.""" From 35b60645811e58d4a50239da560c94172150d8f1 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Sat, 29 Sep 2018 20:53:48 +0200 Subject: [PATCH 103/247] Convert fan component to config entry (#16951) * Conver fan component to config entry * Lint --- homeassistant/components/fan/__init__.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/fan/__init__.py b/homeassistant/components/fan/__init__.py index ec18637f065..36b075747e0 100644 --- a/homeassistant/components/fan/__init__.py +++ b/homeassistant/components/fan/__init__.py @@ -4,7 +4,6 @@ Provides functionality to interact with fans. For more details about this component, please refer to the documentation at https://home-assistant.io/components/fan/ """ -import asyncio from datetime import timedelta import functools as ft import logging @@ -98,13 +97,12 @@ def is_on(hass, entity_id: str = None) -> bool: return state.attributes[ATTR_SPEED] not in [SPEED_OFF, STATE_UNKNOWN] -@asyncio.coroutine -def async_setup(hass, config: dict): +async def async_setup(hass, config: dict): """Expose fan control via statemachine and services.""" - component = EntityComponent( + component = hass.data[DOMAIN] = EntityComponent( _LOGGER, DOMAIN, hass, SCAN_INTERVAL, GROUP_NAME_ALL_FANS) - yield from component.async_setup(config) + await component.async_setup(config) component.async_register_entity_service( SERVICE_TURN_ON, FAN_TURN_ON_SCHEMA, @@ -134,6 +132,16 @@ def async_setup(hass, config: dict): 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 FanEntity(ToggleEntity): """Representation of a fan.""" From 3ddad83a84b260479a72627fce2521fbf6aec457 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Sat, 29 Sep 2018 20:54:57 +0200 Subject: [PATCH 104/247] Add unique_id to MQTT cover (#16950) * Add unique_id to MQTT cover * Fix tests --- homeassistant/components/cover/mqtt.py | 12 +++++- tests/components/cover/test_mqtt.py | 51 +++++++++++++++++++++----- 2 files changed, 52 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/cover/mqtt.py b/homeassistant/components/cover/mqtt.py index a239f4cd4f8..cbc8fbee274 100644 --- a/homeassistant/components/cover/mqtt.py +++ b/homeassistant/components/cover/mqtt.py @@ -5,6 +5,7 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/cover.mqtt/ """ import logging +from typing import Optional import voluptuous as vol @@ -49,6 +50,7 @@ CONF_TILT_MIN = 'tilt_min' CONF_TILT_MAX = 'tilt_max' CONF_TILT_STATE_OPTIMISTIC = 'tilt_optimistic' CONF_TILT_INVERT_STATE = 'tilt_invert_state' +CONF_UNIQUE_ID = 'unique_id' DEFAULT_NAME = 'MQTT Cover' DEFAULT_PAYLOAD_OPEN = 'OPEN' @@ -93,6 +95,7 @@ PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({ default=DEFAULT_TILT_OPTIMISTIC): cv.boolean, vol.Optional(CONF_TILT_INVERT_STATE, default=DEFAULT_TILT_INVERT_STATE): cv.boolean, + vol.Optional(CONF_UNIQUE_ID): cv.string, }).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) @@ -151,6 +154,7 @@ async def _async_setup_entity(hass, config, async_add_entities, config.get(CONF_TILT_INVERT_STATE), config.get(CONF_POSITION_TOPIC), set_position_template, + config.get(CONF_UNIQUE_ID), discovery_hash )]) @@ -165,7 +169,7 @@ class MqttCover(MqttAvailability, MqttDiscoveryUpdate, CoverDevice): optimistic, value_template, tilt_open_position, tilt_closed_position, tilt_min, tilt_max, tilt_optimistic, tilt_invert, position_topic, set_position_template, - discovery_hash): + unique_id: Optional[str], discovery_hash): """Initialize the cover.""" MqttAvailability.__init__(self, availability_topic, qos, payload_available, payload_not_available) @@ -195,6 +199,7 @@ class MqttCover(MqttAvailability, MqttDiscoveryUpdate, CoverDevice): self._tilt_invert = tilt_invert self._position_topic = position_topic self._set_position_template = set_position_template + self._unique_id = unique_id self._discovery_hash = discovery_hash async def async_added_to_hass(self): @@ -412,3 +417,8 @@ class MqttCover(MqttAvailability, MqttDiscoveryUpdate, CoverDevice): if self._tilt_invert: position = self._tilt_max - position + offset return position + + @property + def unique_id(self): + """Return a unique ID.""" + return self._unique_id diff --git a/tests/components/cover/test_mqtt.py b/tests/components/cover/test_mqtt.py index b41a047e3db..355f620520a 100644 --- a/tests/components/cover/test_mqtt.py +++ b/tests/components/cover/test_mqtt.py @@ -11,11 +11,11 @@ from homeassistant.const import ( SERVICE_OPEN_COVER_TILT, SERVICE_SET_COVER_POSITION, SERVICE_SET_COVER_TILT_POSITION, SERVICE_STOP_COVER, STATE_CLOSED, STATE_OPEN, STATE_UNAVAILABLE, STATE_UNKNOWN) -from homeassistant.setup import setup_component +from homeassistant.setup import setup_component, async_setup_component from tests.common import ( get_test_home_assistant, mock_mqtt_component, async_fire_mqtt_message, - fire_mqtt_message, MockConfigEntry) + fire_mqtt_message, MockConfigEntry, async_mock_mqtt_component) class TestCoverMQTT(unittest.TestCase): @@ -614,7 +614,8 @@ class TestCoverMQTT(unittest.TestCase): 'cover.test', 'state-topic', 'command-topic', None, 'tilt-command-topic', 'tilt-status-topic', 0, False, 'OPEN', 'CLOSE', 'OPEN', 'CLOSE', 'STOP', None, None, - False, None, 100, 0, 0, 100, False, False, None, None, None) + False, None, 100, 0, 0, 100, False, False, None, None, None, + None) self.assertEqual(44, mqtt_cover.find_percentage_in_range(44)) @@ -624,7 +625,8 @@ class TestCoverMQTT(unittest.TestCase): 'cover.test', 'state-topic', 'command-topic', None, 'tilt-command-topic', 'tilt-status-topic', 0, False, 'OPEN', 'CLOSE', 'OPEN', 'CLOSE', 'STOP', None, None, - False, None, 180, 80, 80, 180, False, False, None, None, None) + False, None, 180, 80, 80, 180, False, False, None, None, None, + None) self.assertEqual(40, mqtt_cover.find_percentage_in_range(120)) @@ -634,7 +636,8 @@ class TestCoverMQTT(unittest.TestCase): 'cover.test', 'state-topic', 'command-topic', None, 'tilt-command-topic', 'tilt-status-topic', 0, False, 'OPEN', 'CLOSE', 'OPEN', 'CLOSE', 'STOP', None, None, - False, None, 100, 0, 0, 100, False, True, None, None, None) + False, None, 100, 0, 0, 100, False, True, None, None, None, + None) self.assertEqual(56, mqtt_cover.find_percentage_in_range(44)) @@ -644,7 +647,8 @@ class TestCoverMQTT(unittest.TestCase): 'cover.test', 'state-topic', 'command-topic', None, 'tilt-command-topic', 'tilt-status-topic', 0, False, 'OPEN', 'CLOSE', 'OPEN', 'CLOSE', 'STOP', None, None, - False, None, 180, 80, 80, 180, False, True, None, None, None) + False, None, 180, 80, 80, 180, False, True, None, None, None, + None) self.assertEqual(60, mqtt_cover.find_percentage_in_range(120)) @@ -654,7 +658,8 @@ class TestCoverMQTT(unittest.TestCase): 'cover.test', 'state-topic', 'command-topic', None, 'tilt-command-topic', 'tilt-status-topic', 0, False, 'OPEN', 'CLOSE', 'OPEN', 'CLOSE', 'STOP', None, None, - False, None, 100, 0, 0, 100, False, False, None, None, None) + False, None, 100, 0, 0, 100, False, False, None, None, None, + None) self.assertEqual(44, mqtt_cover.find_in_range_from_percent(44)) @@ -664,7 +669,8 @@ class TestCoverMQTT(unittest.TestCase): 'cover.test', 'state-topic', 'command-topic', None, 'tilt-command-topic', 'tilt-status-topic', 0, False, 'OPEN', 'CLOSE', 'OPEN', 'CLOSE', 'STOP', None, None, - False, None, 180, 80, 80, 180, False, False, None, None, None) + False, None, 180, 80, 80, 180, False, False, None, None, None, + None) self.assertEqual(120, mqtt_cover.find_in_range_from_percent(40)) @@ -674,7 +680,8 @@ class TestCoverMQTT(unittest.TestCase): 'cover.test', 'state-topic', 'command-topic', None, 'tilt-command-topic', 'tilt-status-topic', 0, False, 'OPEN', 'CLOSE', 'OPEN', 'CLOSE', 'STOP', None, None, - False, None, 100, 0, 0, 100, False, True, None, None, None) + False, None, 100, 0, 0, 100, False, True, None, None, None, + None) self.assertEqual(44, mqtt_cover.find_in_range_from_percent(56)) @@ -684,7 +691,8 @@ class TestCoverMQTT(unittest.TestCase): 'cover.test', 'state-topic', 'command-topic', None, 'tilt-command-topic', 'tilt-status-topic', 0, False, 'OPEN', 'CLOSE', 'OPEN', 'CLOSE', 'STOP', None, None, - False, None, 180, 80, 80, 180, False, True, None, None, None) + False, None, 180, 80, 80, 180, False, True, None, None, None, + None) self.assertEqual(120, mqtt_cover.find_in_range_from_percent(60)) @@ -779,3 +787,26 @@ async def test_discovery_removal_cover(hass, mqtt_mock, caplog): await hass.async_block_till_done() 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) + assert await async_setup_component(hass, cover.DOMAIN, { + cover.DOMAIN: [{ + 'platform': 'mqtt', + 'name': 'Test 1', + 'state_topic': 'test-topic', + 'unique_id': 'TOTALLY_UNIQUE' + }, { + 'platform': 'mqtt', + 'name': 'Test 2', + 'state_topic': 'test-topic', + 'unique_id': 'TOTALLY_UNIQUE' + }] + }) + + async_fire_mqtt_message(hass, 'test-topic', 'payload') + await hass.async_block_till_done() + + assert len(hass.states.async_entity_ids(cover.DOMAIN)) == 1 From caaf4f56942672cd139ed9fb96d90c134f8c880b Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Sat, 29 Sep 2018 21:22:24 +0200 Subject: [PATCH 105/247] Fix exception during history_stats startup (#16932) * Fix exception during history_stats startup * Do not track changes during startup * Ignore args --- .../components/sensor/history_stats.py | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/sensor/history_stats.py b/homeassistant/components/sensor/history_stats.py index c76d2cefca0..a8d9276edc8 100644 --- a/homeassistant/components/sensor/history_stats.py +++ b/homeassistant/components/sensor/history_stats.py @@ -10,6 +10,7 @@ import math import voluptuous as vol +from homeassistant.core import callback from homeassistant.components import history import homeassistant.helpers.config_validation as cv import homeassistant.util.dt as dt_util @@ -19,7 +20,7 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_START) from homeassistant.exceptions import TemplateError from homeassistant.helpers.entity import Entity -from homeassistant.helpers.event import track_state_change +from homeassistant.helpers.event import async_track_state_change _LOGGER = logging.getLogger(__name__) @@ -94,8 +95,6 @@ class HistoryStatsSensor(Entity): self, hass, entity_id, entity_state, start, end, duration, sensor_type, name): """Initialize the HistoryStats sensor.""" - self._hass = hass - self._entity_id = entity_id self._entity_state = entity_state self._duration = duration @@ -109,15 +108,19 @@ class HistoryStatsSensor(Entity): self.value = None self.count = None - def force_refresh(*args): - """Force the component to refresh.""" - self.schedule_update_ha_state(True) + @callback + def start_refresh(*args): + """Register state tracking.""" + @callback + def force_refresh(*args): + """Force the component to refresh.""" + self.async_schedule_update_ha_state(True) - # Update value when home assistant starts - hass.bus.listen_once(EVENT_HOMEASSISTANT_START, force_refresh) + force_refresh() + async_track_state_change(self.hass, self._entity_id, force_refresh) - # Update value when tracked entity changes its state - track_state_change(hass, entity_id, force_refresh) + # Delay first refresh to keep startup fast + hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_refresh) @property def name(self): From 45fdda3f5d3d4fbaa8af7843be38737581c29cb5 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Sat, 29 Sep 2018 21:22:57 +0200 Subject: [PATCH 106/247] Add unique_id to MQTT fan (#16949) --- homeassistant/components/fan/mqtt.py | 13 ++++++++++++- tests/components/fan/test_mqtt.py | 29 ++++++++++++++++++++++++++-- 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/fan/mqtt.py b/homeassistant/components/fan/mqtt.py index 4ff8e1ec757..3e1ad2704e7 100644 --- a/homeassistant/components/fan/mqtt.py +++ b/homeassistant/components/fan/mqtt.py @@ -5,6 +5,7 @@ For more details about this platform, please refer to the documentation https://home-assistant.io/components/fan.mqtt/ """ import logging +from typing import Optional import voluptuous as vol @@ -41,6 +42,7 @@ CONF_PAYLOAD_LOW_SPEED = 'payload_low_speed' CONF_PAYLOAD_MEDIUM_SPEED = 'payload_medium_speed' CONF_PAYLOAD_HIGH_SPEED = 'payload_high_speed' CONF_SPEED_LIST = 'speeds' +CONF_UNIQUE_ID = 'unique_id' DEFAULT_NAME = 'MQTT Fan' DEFAULT_PAYLOAD_ON = 'ON' @@ -74,6 +76,7 @@ PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({ default=[SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH]): cv.ensure_list, vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, + vol.Optional(CONF_UNIQUE_ID): cv.string, }).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) @@ -120,6 +123,7 @@ async def async_setup_platform(hass: HomeAssistantType, config: ConfigType, config.get(CONF_AVAILABILITY_TOPIC), config.get(CONF_PAYLOAD_AVAILABLE), config.get(CONF_PAYLOAD_NOT_AVAILABLE), + config.get(CONF_UNIQUE_ID), discovery_hash, )]) @@ -129,7 +133,8 @@ class MqttFan(MqttAvailability, MqttDiscoveryUpdate, FanEntity): def __init__(self, name, topic, templates, qos, retain, payload, speed_list, optimistic, availability_topic, payload_available, - payload_not_available, discovery_hash): + payload_not_available, unique_id: Optional[str], + discovery_hash): """Initialize the MQTT fan.""" MqttAvailability.__init__(self, availability_topic, qos, payload_available, payload_not_available) @@ -154,6 +159,7 @@ class MqttFan(MqttAvailability, MqttDiscoveryUpdate, FanEntity): is not None and SUPPORT_OSCILLATE) self._supported_features |= (topic[CONF_SPEED_STATE_TOPIC] is not None and SUPPORT_SET_SPEED) + self._unique_id = unique_id self._discovery_hash = discovery_hash async def async_added_to_hass(self): @@ -323,3 +329,8 @@ class MqttFan(MqttAvailability, MqttDiscoveryUpdate, FanEntity): if self._optimistic_oscillation: self._oscillation = oscillating self.async_schedule_update_ha_state() + + @property + def unique_id(self): + """Return a unique ID.""" + return self._unique_id diff --git a/tests/components/fan/test_mqtt.py b/tests/components/fan/test_mqtt.py index 5f59ebe4a76..7434e5aa1c9 100644 --- a/tests/components/fan/test_mqtt.py +++ b/tests/components/fan/test_mqtt.py @@ -1,14 +1,14 @@ """Test MQTT fans.""" import unittest -from homeassistant.setup import setup_component +from homeassistant.setup import setup_component, async_setup_component from homeassistant.components import fan from homeassistant.components.mqtt.discovery import async_start from homeassistant.const import ATTR_ASSUMED_STATE, STATE_UNAVAILABLE from tests.common import ( mock_mqtt_component, async_fire_mqtt_message, fire_mqtt_message, - get_test_home_assistant) + get_test_home_assistant, async_mock_mqtt_component) class TestMqttFan(unittest.TestCase): @@ -125,3 +125,28 @@ async def test_discovery_removal_fan(hass, mqtt_mock, caplog): await hass.async_block_till_done() 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) + assert await async_setup_component(hass, fan.DOMAIN, { + fan.DOMAIN: [{ + 'platform': 'mqtt', + 'name': 'Test 1', + 'state_topic': 'test-topic', + 'command_topic': 'test-topic', + 'unique_id': 'TOTALLY_UNIQUE' + }, { + 'platform': 'mqtt', + 'name': 'Test 2', + 'state_topic': 'test-topic', + 'command_topic': 'test-topic', + 'unique_id': 'TOTALLY_UNIQUE' + }] + }) + + async_fire_mqtt_message(hass, 'test-topic', 'payload') + await hass.async_block_till_done() + + assert len(hass.states.async_entity_ids(fan.DOMAIN)) == 1 From 70ce9bb7bc15b1a66bb1d21598efd0bb8b102522 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Sat, 29 Sep 2018 22:01:04 +0200 Subject: [PATCH 107/247] Add pressure sensor device class (#16965) * Add pressure sensor device class * Undo github desktop line exclude --- homeassistant/components/sensor/__init__.py | 3 ++- homeassistant/components/sensor/xiaomi_aqara.py | 4 ++-- homeassistant/const.py | 1 + 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index 948f844cfd4..be599cc295a 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -14,7 +14,7 @@ from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa from homeassistant.const import ( DEVICE_CLASS_BATTERY, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, - DEVICE_CLASS_TEMPERATURE) + DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_PRESSURE) _LOGGER = logging.getLogger(__name__) @@ -28,6 +28,7 @@ DEVICE_CLASSES = [ DEVICE_CLASS_HUMIDITY, # % of humidity in the air DEVICE_CLASS_ILLUMINANCE, # current light level (lx/lm) DEVICE_CLASS_TEMPERATURE, # temperature (C/F) + DEVICE_CLASS_PRESSURE, # pressure (hPa/mbar) ] DEVICE_CLASSES_SCHEMA = vol.All(vol.Lower, vol.In(DEVICE_CLASSES)) diff --git a/homeassistant/components/sensor/xiaomi_aqara.py b/homeassistant/components/sensor/xiaomi_aqara.py index 8a3a11db051..31366fe0097 100644 --- a/homeassistant/components/sensor/xiaomi_aqara.py +++ b/homeassistant/components/sensor/xiaomi_aqara.py @@ -5,7 +5,7 @@ from homeassistant.components.xiaomi_aqara import (PY_XIAOMI_GATEWAY, XiaomiDevice) from homeassistant.const import ( DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_TEMPERATURE, - TEMP_CELSIUS) + TEMP_CELSIUS, DEVICE_CLASS_PRESSURE) _LOGGER = logging.getLogger(__name__) @@ -14,7 +14,7 @@ SENSOR_TYPES = { 'humidity': ['%', None, DEVICE_CLASS_HUMIDITY], 'illumination': ['lm', None, DEVICE_CLASS_ILLUMINANCE], 'lux': ['lx', None, DEVICE_CLASS_ILLUMINANCE], - 'pressure': ['hPa', 'mdi:gauge', None] + 'pressure': ['hPa', None, DEVICE_CLASS_PRESSURE] } diff --git a/homeassistant/const.py b/homeassistant/const.py index 1d3adf7ee45..93ab0deeba2 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -172,6 +172,7 @@ DEVICE_CLASS_BATTERY = 'battery' DEVICE_CLASS_HUMIDITY = 'humidity' DEVICE_CLASS_ILLUMINANCE = 'illuminance' DEVICE_CLASS_TEMPERATURE = 'temperature' +DEVICE_CLASS_PRESSURE = 'pressure' # #### STATES #### STATE_ON = 'on' From 8b1bdda0faec9f0a8ca58c88738d8ab63d9f73e5 Mon Sep 17 00:00:00 2001 From: Rohan Kapoor Date: Sun, 30 Sep 2018 00:21:07 -0700 Subject: [PATCH 108/247] Bump zm-py to 0.0.4 (#16979) --- homeassistant/components/zoneminder.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zoneminder.py b/homeassistant/components/zoneminder.py index 53d6d8b2536..b7f43177200 100644 --- a/homeassistant/components/zoneminder.py +++ b/homeassistant/components/zoneminder.py @@ -15,7 +15,7 @@ import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['zm-py==0.0.3'] +REQUIREMENTS = ['zm-py==0.0.4'] CONF_PATH_ZMS = 'path_zms' diff --git a/requirements_all.txt b/requirements_all.txt index eeee16a6138..53d38f09f3b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1574,4 +1574,4 @@ zigpy-xbee==0.1.1 zigpy==0.2.0 # homeassistant.components.zoneminder -zm-py==0.0.3 +zm-py==0.0.4 From 7f47d601f172689032020b2b8c1a4d6078791d81 Mon Sep 17 00:00:00 2001 From: Greg Laabs Date: Sun, 30 Sep 2018 00:21:27 -0700 Subject: [PATCH 109/247] Fix ISY blocking bug (#16978) This fix results in `is_on` returning False if the state is unknown (which was a bug in 0.79). --- homeassistant/components/light/isy994.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/light/isy994.py b/homeassistant/components/light/isy994.py index 4349bfa1467..d54aa3cd4ce 100644 --- a/homeassistant/components/light/isy994.py +++ b/homeassistant/components/light/isy994.py @@ -31,12 +31,14 @@ class ISYLightDevice(ISYDevice, Light): @property def is_on(self) -> bool: """Get whether the ISY994 light is on.""" + if self.is_unknown(): + return False return self.value != 0 @property def brightness(self) -> float: """Get the brightness of the ISY994 light.""" - return self.value + return None if self.is_unknown() else self.value def turn_off(self, **kwargs) -> None: """Send the turn off command to the ISY994 light device.""" From 06d959ed43ac0f04d38faecedf742e2eaf5df9c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sun, 30 Sep 2018 11:20:10 +0300 Subject: [PATCH 110/247] Upgrade pytest to 3.8.1 (#16980) --- 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 15e06c4e53d..87450f5422d 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -13,5 +13,5 @@ pytest-aiohttp==0.3.0 pytest-cov==2.5.1 pytest-sugar==0.9.1 pytest-timeout==1.3.2 -pytest==3.8.0 +pytest==3.8.1 requests_mock==1.5.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 736310a719a..a04f0570ee9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -14,7 +14,7 @@ pytest-aiohttp==0.3.0 pytest-cov==2.5.1 pytest-sugar==0.9.1 pytest-timeout==1.3.2 -pytest==3.8.0 +pytest==3.8.1 requests_mock==1.5.2 From f5632a5da5f1ad00c27ee423344dada6c099844e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 30 Sep 2018 14:45:48 +0200 Subject: [PATCH 111/247] Add webhook + IFTTT example (#16817) * Add webhook + IFTTT example * Abort if not externally accessible * Abort on local url * Add description to create entry * Make body optional * Allow ifttt setup without config * Add tests * Lint * Fix Lint + Tests * Fix typing --- homeassistant/components/auth/indieauth.py | 19 +-- homeassistant/components/ifttt.py | 74 ---------- .../components/ifttt/.translations/en.json | 18 +++ homeassistant/components/ifttt/__init__.py | 135 ++++++++++++++++++ homeassistant/components/ifttt/strings.json | 18 +++ homeassistant/components/webhook.py | 94 ++++++++++++ homeassistant/config_entries.py | 1 + homeassistant/data_entry_flow.py | 7 +- homeassistant/util/network.py | 22 +++ .../components/config/test_config_entries.py | 4 + tests/components/ifttt/__init__.py | 1 + tests/components/ifttt/test_init.py | 48 +++++++ tests/components/test_webhook.py | 98 +++++++++++++ 13 files changed, 448 insertions(+), 91 deletions(-) delete mode 100644 homeassistant/components/ifttt.py create mode 100644 homeassistant/components/ifttt/.translations/en.json create mode 100644 homeassistant/components/ifttt/__init__.py create mode 100644 homeassistant/components/ifttt/strings.json create mode 100644 homeassistant/components/webhook.py create mode 100644 homeassistant/util/network.py create mode 100644 tests/components/ifttt/__init__.py create mode 100644 tests/components/ifttt/test_init.py create mode 100644 tests/components/test_webhook.py diff --git a/homeassistant/components/auth/indieauth.py b/homeassistant/components/auth/indieauth.py index bcf73258ffa..30432a612a4 100644 --- a/homeassistant/components/auth/indieauth.py +++ b/homeassistant/components/auth/indieauth.py @@ -1,24 +1,13 @@ """Helpers to resolve client ID/secret.""" import asyncio +from ipaddress import ip_address from html.parser import HTMLParser -from ipaddress import ip_address, ip_network from urllib.parse import urlparse, urljoin import aiohttp from aiohttp.client_exceptions import ClientError -# IP addresses of loopback interfaces -ALLOWED_IPS = ( - ip_address('127.0.0.1'), - ip_address('::1'), -) - -# RFC1918 - Address allocation for Private Internets -ALLOWED_NETWORKS = ( - ip_network('10.0.0.0/8'), - ip_network('172.16.0.0/12'), - ip_network('192.168.0.0/16'), -) +from homeassistant.util.network import is_local async def verify_redirect_uri(hass, client_id, redirect_uri): @@ -185,9 +174,7 @@ def _parse_client_id(client_id): # Not an ip address pass - if (address is None or - address in ALLOWED_IPS or - any(address in network for network in ALLOWED_NETWORKS)): + if address is None or is_local(address): return parts raise ValueError('Hostname should be a domain name or local IP address') diff --git a/homeassistant/components/ifttt.py b/homeassistant/components/ifttt.py deleted file mode 100644 index 9497282ab21..00000000000 --- a/homeassistant/components/ifttt.py +++ /dev/null @@ -1,74 +0,0 @@ -""" -Support to trigger Maker IFTTT recipes. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/ifttt/ -""" -import logging - -import requests -import voluptuous as vol - -import homeassistant.helpers.config_validation as cv - -REQUIREMENTS = ['pyfttt==0.3'] - -_LOGGER = logging.getLogger(__name__) - -ATTR_EVENT = 'event' -ATTR_VALUE1 = 'value1' -ATTR_VALUE2 = 'value2' -ATTR_VALUE3 = 'value3' - -CONF_KEY = 'key' - -DOMAIN = 'ifttt' - -SERVICE_TRIGGER = 'trigger' - -SERVICE_TRIGGER_SCHEMA = vol.Schema({ - vol.Required(ATTR_EVENT): cv.string, - vol.Optional(ATTR_VALUE1): cv.string, - vol.Optional(ATTR_VALUE2): cv.string, - vol.Optional(ATTR_VALUE3): cv.string, -}) - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_KEY): cv.string, - }), -}, extra=vol.ALLOW_EXTRA) - - -def trigger(hass, event, value1=None, value2=None, value3=None): - """Trigger a Maker IFTTT recipe.""" - data = { - ATTR_EVENT: event, - ATTR_VALUE1: value1, - ATTR_VALUE2: value2, - ATTR_VALUE3: value3, - } - hass.services.call(DOMAIN, SERVICE_TRIGGER, data) - - -def setup(hass, config): - """Set up the IFTTT service component.""" - key = config[DOMAIN][CONF_KEY] - - def trigger_service(call): - """Handle IFTTT trigger service calls.""" - event = call.data[ATTR_EVENT] - value1 = call.data.get(ATTR_VALUE1) - value2 = call.data.get(ATTR_VALUE2) - value3 = call.data.get(ATTR_VALUE3) - - try: - import pyfttt - pyfttt.send_event(key, event, value1, value2, value3) - except requests.exceptions.RequestException: - _LOGGER.exception("Error communicating with IFTTT") - - hass.services.register(DOMAIN, SERVICE_TRIGGER, trigger_service, - schema=SERVICE_TRIGGER_SCHEMA) - - return True diff --git a/homeassistant/components/ifttt/.translations/en.json b/homeassistant/components/ifttt/.translations/en.json new file mode 100644 index 00000000000..dae4b24de47 --- /dev/null +++ b/homeassistant/components/ifttt/.translations/en.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Your Home Assistant instance needs to be accessible from the internet to receive IFTTT messages.", + "one_instance_allowed": "Only a single instance is necessary." + }, + "create_entry": { + "default": "To send events to Home Assistant, you will need to use the \"Make a web request\" action from the [IFTTT Webhook applet]({applet_url}).\n\nFill in the following info:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\nSee [the documentation]({docs_url}) on how to configure automations to handle incoming data." + }, + "step": { + "user": { + "description": "Are you sure you want to set up IFTTT?", + "title": "Set up the IFTTT Webhook Applet" + } + }, + "title": "IFTTT" + } +} \ No newline at end of file diff --git a/homeassistant/components/ifttt/__init__.py b/homeassistant/components/ifttt/__init__.py new file mode 100644 index 00000000000..534217a7ba2 --- /dev/null +++ b/homeassistant/components/ifttt/__init__.py @@ -0,0 +1,135 @@ +""" +Support to trigger Maker IFTTT recipes. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/ifttt/ +""" +from ipaddress import ip_address +import logging +from urllib.parse import urlparse + +import requests +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant import config_entries +from homeassistant.util.network import is_local + +REQUIREMENTS = ['pyfttt==0.3'] +DEPENDENCIES = ['webhook'] + +_LOGGER = logging.getLogger(__name__) + +EVENT_RECEIVED = 'ifttt_webhook_received' + +ATTR_EVENT = 'event' +ATTR_VALUE1 = 'value1' +ATTR_VALUE2 = 'value2' +ATTR_VALUE3 = 'value3' + +CONF_KEY = 'key' +CONF_WEBHOOK_ID = 'webhook_id' + +DOMAIN = 'ifttt' + +SERVICE_TRIGGER = 'trigger' + +SERVICE_TRIGGER_SCHEMA = vol.Schema({ + vol.Required(ATTR_EVENT): cv.string, + vol.Optional(ATTR_VALUE1): cv.string, + vol.Optional(ATTR_VALUE2): cv.string, + vol.Optional(ATTR_VALUE3): cv.string, +}) + +CONFIG_SCHEMA = vol.Schema({ + vol.Optional(DOMAIN): vol.Schema({ + vol.Required(CONF_KEY): cv.string, + }), +}, extra=vol.ALLOW_EXTRA) + + +async def async_setup(hass, config): + """Set up the IFTTT service component.""" + if DOMAIN not in config: + return True + + key = config[DOMAIN][CONF_KEY] + + def trigger_service(call): + """Handle IFTTT trigger service calls.""" + event = call.data[ATTR_EVENT] + value1 = call.data.get(ATTR_VALUE1) + value2 = call.data.get(ATTR_VALUE2) + value3 = call.data.get(ATTR_VALUE3) + + try: + import pyfttt + pyfttt.send_event(key, event, value1, value2, value3) + except requests.exceptions.RequestException: + _LOGGER.exception("Error communicating with IFTTT") + + hass.services.async_register(DOMAIN, SERVICE_TRIGGER, trigger_service, + schema=SERVICE_TRIGGER_SCHEMA) + + return True + + +async def handle_webhook(hass, webhook_id, data): + """Handle webhook callback.""" + if isinstance(data, dict): + data['webhook_id'] = webhook_id + hass.bus.async_fire(EVENT_RECEIVED, data) + + +async def async_setup_entry(hass, entry): + """Configure based on config entry.""" + hass.components.webhook.async_register( + entry.data['webhook_id'], handle_webhook) + return True + + +async def async_unload_entry(hass, entry): + """Unload a config entry.""" + hass.components.webhook.async_unregister(entry.data['webhook_id']) + return True + + +@config_entries.HANDLERS.register(DOMAIN) +class ConfigFlow(config_entries.ConfigFlow): + """Handle an IFTTT config flow.""" + + async def async_step_user(self, user_input=None): + """Handle a user initiated set up flow.""" + if self._async_current_entries(): + return self.async_abort(reason='one_instance_allowed') + + try: + url_parts = urlparse(self.hass.config.api.base_url) + + if is_local(ip_address(url_parts.hostname)): + return self.async_abort(reason='not_internet_accessible') + except ValueError: + # If it's not an IP address, it's very likely publicly accessible + pass + + if user_input is None: + return self.async_show_form( + step_id='user', + ) + + webhook_id = self.hass.components.webhook.async_generate_id() + webhook_url = \ + self.hass.components.webhook.async_generate_url(webhook_id) + + return self.async_create_entry( + title='IFTTT Webhook', + data={ + CONF_WEBHOOK_ID: webhook_id + }, + description_placeholders={ + 'applet_url': 'https://ifttt.com/maker_webhooks', + 'webhook_url': webhook_url, + 'docs_url': + 'https://www.home-assistant.io/components/ifttt/' + } + ) diff --git a/homeassistant/components/ifttt/strings.json b/homeassistant/components/ifttt/strings.json new file mode 100644 index 00000000000..9fc47504b9b --- /dev/null +++ b/homeassistant/components/ifttt/strings.json @@ -0,0 +1,18 @@ +{ + "config": { + "title": "IFTTT", + "step": { + "user": { + "title": "Set up the IFTTT Webhook Applet", + "description": "Are you sure you want to set up IFTTT?" + } + }, + "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 IFTTT messages." + }, + "create_entry": { + "default": "To send events to Home Assistant, you will need to use the \"Make a web request\" action from the [IFTTT Webhook applet]({applet_url}).\n\nFill in the following info:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\nSee [the documentation]({docs_url}) on how to configure automations to handle incoming data." + } + } +} diff --git a/homeassistant/components/webhook.py b/homeassistant/components/webhook.py new file mode 100644 index 00000000000..0e44ffbab25 --- /dev/null +++ b/homeassistant/components/webhook.py @@ -0,0 +1,94 @@ +"""Webhooks for Home Assistant. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/webhook/ +""" +import json +import logging + +from aiohttp.web import Response + +from homeassistant.core import callback +from homeassistant.loader import bind_hass +from homeassistant.auth.util import generate_secret +from homeassistant.components.http.view import HomeAssistantView + +DOMAIN = 'webhook' +DEPENDENCIES = ['http'] +_LOGGER = logging.getLogger(__name__) + + +@callback +@bind_hass +def async_register(hass, webhook_id, handler): + """Register a webhook.""" + handlers = hass.data.setdefault(DOMAIN, {}) + + if webhook_id in handlers: + raise ValueError('Handler is already defined!') + + handlers[webhook_id] = handler + + +@callback +@bind_hass +def async_unregister(hass, webhook_id): + """Remove a webhook.""" + handlers = hass.data.setdefault(DOMAIN, {}) + handlers.pop(webhook_id, None) + + +@callback +def async_generate_id(): + """Generate a webhook_id.""" + return generate_secret(entropy=32) + + +@callback +@bind_hass +def async_generate_url(hass, webhook_id): + """Generate a webhook_id.""" + return "{}/api/webhook/{}".format(hass.config.api.base_url, webhook_id) + + +async def async_setup(hass, config): + """Initialize the webhook component.""" + hass.http.register_view(WebhookView) + return True + + +class WebhookView(HomeAssistantView): + """Handle incoming webhook requests.""" + + url = "/api/webhook/{webhook_id}" + name = "api:webhook" + requires_auth = False + + async def post(self, request, webhook_id): + """Handle webhook call.""" + hass = request.app['hass'] + handlers = hass.data.setdefault(DOMAIN, {}) + handler = handlers.get(webhook_id) + + # Always respond successfully to not give away if a hook exists or not. + if handler is None: + _LOGGER.warning( + 'Received message for unregistered webhook %s', webhook_id) + return Response(status=200) + + body = await request.text() + try: + data = json.loads(body) if body else {} + except ValueError: + _LOGGER.warning( + 'Received webhook %s with invalid JSON', webhook_id) + return Response(status=200) + + try: + response = await handler(hass, webhook_id, data) + if response is None: + response = Response(status=200) + return response + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Error processing webhook %s", webhook_id) + return Response(status=200) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 83bf9d22de3..fcc8a1f92ac 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -141,6 +141,7 @@ FLOWS = [ 'deconz', 'homematicip_cloud', 'hue', + 'ifttt', 'ios', 'mqtt', 'nest', diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index ecf9850a67c..57265cf696d 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -153,7 +153,10 @@ class FlowHandler: } @callback - def async_create_entry(self, *, title: str, data: Dict) -> Dict: + def async_create_entry(self, *, title: str, data: Dict, + description: Optional[str] = None, + description_placeholders: Optional[Dict] = None) \ + -> Dict: """Finish config flow and create a config entry.""" return { 'version': self.VERSION, @@ -162,6 +165,8 @@ class FlowHandler: 'handler': self.handler, 'title': title, 'data': data, + 'description': description, + 'description_placeholders': description_placeholders, } @callback diff --git a/homeassistant/util/network.py b/homeassistant/util/network.py new file mode 100644 index 00000000000..48840f339c1 --- /dev/null +++ b/homeassistant/util/network.py @@ -0,0 +1,22 @@ +"""Network utilities.""" +from ipaddress import IPv4Address, IPv6Address, ip_address, ip_network +from typing import Union + +# IP addresses of loopback interfaces +LOCAL_IPS = ( + ip_address('127.0.0.1'), + ip_address('::1'), +) + +# RFC1918 - Address allocation for Private Internets +LOCAL_NETWORKS = ( + ip_network('10.0.0.0/8'), + ip_network('172.16.0.0/12'), + ip_network('192.168.0.0/16'), +) + + +def is_local(address: Union[IPv4Address, IPv6Address]) -> bool: + """Check if an address is local.""" + return address in LOCAL_IPS or \ + any(address in network for network in LOCAL_NETWORKS) diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index 1e3b507727c..67d7eebbfec 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -206,6 +206,8 @@ def test_create_account(hass, client): 'title': 'Test Entry', 'type': 'create_entry', 'version': 1, + 'description': None, + 'description_placeholders': None, } @@ -266,6 +268,8 @@ def test_two_step_flow(hass, client): 'type': 'create_entry', 'title': 'user-title', 'version': 1, + 'description': None, + 'description_placeholders': None, } diff --git a/tests/components/ifttt/__init__.py b/tests/components/ifttt/__init__.py new file mode 100644 index 00000000000..2fe2f40276c --- /dev/null +++ b/tests/components/ifttt/__init__.py @@ -0,0 +1 @@ +"""Tests for the IFTTT component.""" diff --git a/tests/components/ifttt/test_init.py b/tests/components/ifttt/test_init.py new file mode 100644 index 00000000000..61d6654ba55 --- /dev/null +++ b/tests/components/ifttt/test_init.py @@ -0,0 +1,48 @@ +"""Test the init file of IFTTT.""" +from unittest.mock import Mock, patch + +from homeassistant import data_entry_flow +from homeassistant.core import callback +from homeassistant.components import ifttt + + +async def test_config_flow_registers_webhook(hass, aiohttp_client): + """Test setting up IFTTT and sending webhook.""" + with patch('homeassistant.util.get_local_ip', return_value='example.com'): + result = await hass.config_entries.flow.async_init('ifttt', 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 + webhook_id = result['result'].data['webhook_id'] + + ifttt_events = [] + + @callback + def handle_event(event): + """Handle IFTTT event.""" + ifttt_events.append(event) + + hass.bus.async_listen(ifttt.EVENT_RECEIVED, handle_event) + + client = await aiohttp_client(hass.http.app) + await client.post('/api/webhook/{}'.format(webhook_id), json={ + 'hello': 'ifttt' + }) + + assert len(ifttt_events) == 1 + assert ifttt_events[0].data['webhook_id'] == webhook_id + assert ifttt_events[0].data['hello'] == 'ifttt' + + +async def test_config_flow_aborts_external_url(hass, aiohttp_client): + """Test setting up IFTTT and sending webhook.""" + hass.config.api = Mock(base_url='http://192.168.1.10') + result = await hass.config_entries.flow.async_init('ifttt', context={ + 'source': 'user' + }) + assert result['type'] == data_entry_flow.RESULT_TYPE_ABORT + assert result['reason'] == 'not_internet_accessible' diff --git a/tests/components/test_webhook.py b/tests/components/test_webhook.py new file mode 100644 index 00000000000..c87687292a8 --- /dev/null +++ b/tests/components/test_webhook.py @@ -0,0 +1,98 @@ +"""Test the webhook component.""" +from unittest.mock import Mock + +import pytest + +from homeassistant.setup import async_setup_component + + +@pytest.fixture +def mock_client(hass, aiohttp_client): + """Create http client for webhooks.""" + hass.loop.run_until_complete(async_setup_component(hass, 'webhook', {})) + return hass.loop.run_until_complete(aiohttp_client(hass.http.app)) + + +async def test_unregistering_webhook(hass, mock_client): + """Test unregistering a webhook.""" + hooks = [] + webhook_id = hass.components.webhook.async_generate_id() + + async def handle(*args): + """Handle webhook.""" + hooks.append(args) + + hass.components.webhook.async_register(webhook_id, handle) + + resp = await mock_client.post('/api/webhook/{}'.format(webhook_id)) + assert resp.status == 200 + assert len(hooks) == 1 + + hass.components.webhook.async_unregister(webhook_id) + + resp = await mock_client.post('/api/webhook/{}'.format(webhook_id)) + assert resp.status == 200 + assert len(hooks) == 1 + + +async def test_generate_webhook_url(hass): + """Test we generate a webhook url correctly.""" + hass.config.api = Mock(base_url='https://example.com') + url = hass.components.webhook.async_generate_url('some_id') + + assert url == 'https://example.com/api/webhook/some_id' + + +async def test_posting_webhook_nonexisting(hass, mock_client): + """Test posting to a nonexisting webhook.""" + resp = await mock_client.post('/api/webhook/non-existing') + assert resp.status == 200 + + +async def test_posting_webhook_invalid_json(hass, mock_client): + """Test posting to a nonexisting webhook.""" + hass.components.webhook.async_register('hello', None) + resp = await mock_client.post('/api/webhook/hello', data='not-json') + assert resp.status == 200 + + +async def test_posting_webhook_json(hass, mock_client): + """Test posting a webhook with JSON data.""" + hooks = [] + webhook_id = hass.components.webhook.async_generate_id() + + async def handle(*args): + """Handle webhook.""" + hooks.append(args) + + hass.components.webhook.async_register(webhook_id, handle) + + resp = await mock_client.post('/api/webhook/{}'.format(webhook_id), json={ + 'data': True + }) + assert resp.status == 200 + assert len(hooks) == 1 + assert hooks[0][0] is hass + assert hooks[0][1] == webhook_id + assert hooks[0][2] == { + 'data': True + } + + +async def test_posting_webhook_no_data(hass, mock_client): + """Test posting a webhook with no data.""" + hooks = [] + webhook_id = hass.components.webhook.async_generate_id() + + async def handle(*args): + """Handle webhook.""" + hooks.append(args) + + hass.components.webhook.async_register(webhook_id, handle) + + resp = await mock_client.post('/api/webhook/{}'.format(webhook_id)) + assert resp.status == 200 + assert len(hooks) == 1 + assert hooks[0][0] is hass + assert hooks[0][1] == webhook_id + assert hooks[0][2] == {} From 1b7bfec2477e98be8b1428085e5d40d1e9943c3e Mon Sep 17 00:00:00 2001 From: Totoo Date: Sun, 30 Sep 2018 15:17:39 +0200 Subject: [PATCH 112/247] Google Maps supports battery level and charging. (#16969) * Google Maps supports battery level and charging. With 3.0.2 locationsharinglib now the battery level and the charging attributes are available. * Update google_maps.py fix too long line error * Update google_maps.py Fix multi line import, and line length limit * Update gen_requirements_all.py Add locationsharinglib to gen_requirements_all * update requirements_all * Last try to fix requirements_all... --- homeassistant/components/device_tracker/google_maps.py | 8 ++++++-- requirements_all.txt | 2 +- requirements_test_all.txt | 3 +++ script/gen_requirements_all.py | 1 + 4 files changed, 11 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/device_tracker/google_maps.py b/homeassistant/components/device_tracker/google_maps.py index 170d3de6800..c0dcd2f00a7 100644 --- a/homeassistant/components/device_tracker/google_maps.py +++ b/homeassistant/components/device_tracker/google_maps.py @@ -11,13 +11,15 @@ import voluptuous as vol from homeassistant.components.device_tracker import ( PLATFORM_SCHEMA, SOURCE_TYPE_GPS) -from homeassistant.const import ATTR_ID, CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import ( + ATTR_ID, CONF_PASSWORD, CONF_USERNAME, ATTR_BATTERY_CHARGING, + ATTR_BATTERY_LEVEL) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import track_time_interval from homeassistant.helpers.typing import ConfigType from homeassistant.util import slugify, dt as dt_util -REQUIREMENTS = ['locationsharinglib==2.0.11'] +REQUIREMENTS = ['locationsharinglib==3.0.2'] _LOGGER = logging.getLogger(__name__) @@ -94,6 +96,8 @@ class GoogleMapsScanner: ATTR_ID: person.id, ATTR_LAST_SEEN: dt_util.as_utc(person.datetime), ATTR_NICKNAME: person.nickname, + ATTR_BATTERY_CHARGING: person.charging, + ATTR_BATTERY_LEVEL: person.battery_level } self.see( dev_id=dev_id, diff --git a/requirements_all.txt b/requirements_all.txt index 53d38f09f3b..638599b9642 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -558,7 +558,7 @@ liveboxplaytv==2.0.2 lmnotify==0.0.4 # homeassistant.components.device_tracker.google_maps -locationsharinglib==2.0.11 +locationsharinglib==3.0.2 # homeassistant.components.logi_circle logi_circle==0.1.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a04f0570ee9..540a200ad32 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -103,6 +103,9 @@ libpurecoollink==0.4.2 # homeassistant.components.media_player.soundtouch libsoundtouch==0.7.2 +# homeassistant.components.device_tracker.google_maps +locationsharinglib==3.0.2 + # homeassistant.components.sensor.mfi # homeassistant.components.switch.mfi mficlient==0.3.0 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 7493e523273..3fca95e1adf 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -63,6 +63,7 @@ TEST_REQUIREMENTS = ( 'influxdb', 'libpurecoollink', 'libsoundtouch', + 'locationsharinglib', 'mficlient', 'numpy', 'paho-mqtt', From 0a2b2667426c628d6cebc1f489355696071c8447 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 30 Sep 2018 21:36:27 +0200 Subject: [PATCH 113/247] Fix MQTT certificates (#16999) --- homeassistant/components/mqtt/__init__.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 335b4d31acb..62bbb8dc9c5 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -426,25 +426,22 @@ async def async_setup_entry(hass, entry): keepalive = conf[CONF_KEEPALIVE] username = conf.get(CONF_USERNAME) password = conf.get(CONF_PASSWORD) + certificate = conf.get(CONF_CERTIFICATE) client_key = conf.get(CONF_CLIENT_KEY) client_cert = conf.get(CONF_CLIENT_CERT) tls_insecure = conf.get(CONF_TLS_INSECURE) protocol = conf[CONF_PROTOCOL] # For cloudmqtt.com, secured connection, auto fill in certificate - if (conf.get(CONF_CERTIFICATE) is None and - 19999 < conf[CONF_PORT] < 30000 and - conf[CONF_BROKER].endswith('.cloudmqtt.com')): + if (certificate is None and 19999 < conf[CONF_PORT] < 30000 and + broker.endswith('.cloudmqtt.com')): certificate = os.path.join( os.path.dirname(__file__), 'addtrustexternalcaroot.crt') # When the certificate is set to auto, use bundled certs from requests - elif conf.get(CONF_CERTIFICATE) == 'auto': + elif certificate == 'auto': certificate = requests.certs.where() - else: - certificate = None - if CONF_WILL_MESSAGE in conf: will_message = Message(**conf[CONF_WILL_MESSAGE]) else: From 940d5fb2eeaf8103ca990c84a05869d849375074 Mon Sep 17 00:00:00 2001 From: Patrik <21142447+ggravlingen@users.noreply.github.com> Date: Sun, 30 Sep 2018 22:22:07 +0200 Subject: [PATCH 114/247] Add basic support for Tradfri switches (#17007) * Initial commit * Sockets have been moved to separate component * Sockets have been moved to separate component * Fix const PLATFORM_SCHEMA * Fix unique id * Fix async_create_task * Fix PLATFORM_SCHEMA * Fix typo * Remove pylint disable --- homeassistant/components/sensor/tradfri.py | 3 +- homeassistant/components/switch/tradfri.py | 137 +++++++++++++++++++ homeassistant/components/tradfri/__init__.py | 5 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 145 insertions(+), 4 deletions(-) create mode 100644 homeassistant/components/switch/tradfri.py diff --git a/homeassistant/components/sensor/tradfri.py b/homeassistant/components/sensor/tradfri.py index 86d0c1abc19..769857cb6df 100644 --- a/homeassistant/components/sensor/tradfri.py +++ b/homeassistant/components/sensor/tradfri.py @@ -26,7 +26,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities): devices_commands = await api(gateway.get_devices()) all_devices = await api(devices_commands) - devices = (dev for dev in all_devices if not dev.has_light_control) + devices = (dev for dev in all_devices if not dev.has_light_control and + not dev.has_socket_control) async_add_entities(TradfriDevice(device, api) for device in devices) diff --git a/homeassistant/components/switch/tradfri.py b/homeassistant/components/switch/tradfri.py new file mode 100644 index 00000000000..74997332b07 --- /dev/null +++ b/homeassistant/components/switch/tradfri.py @@ -0,0 +1,137 @@ +""" +Support for the IKEA Tradfri platform. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/switch.tradfri/ +""" +import logging + +from homeassistant.core import callback +from homeassistant.components.switch import SwitchDevice +from homeassistant.components.tradfri import ( + KEY_GATEWAY, KEY_API, DOMAIN as TRADFRI_DOMAIN) +from homeassistant.components.tradfri.const import ( + CONF_GATEWAY_ID) + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['tradfri'] +IKEA = 'IKEA of Sweden' +TRADFRI_SWITCH_MANAGER = 'Tradfri Switch Manager' + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Load Tradfri switches based on a config entry.""" + gateway_id = config_entry.data[CONF_GATEWAY_ID] + api = hass.data[KEY_API][config_entry.entry_id] + gateway = hass.data[KEY_GATEWAY][config_entry.entry_id] + + devices_commands = await api(gateway.get_devices()) + devices = await api(devices_commands) + switches = [dev for dev in devices if dev.has_socket_control] + if switches: + async_add_entities( + TradfriSwitch(switch, api, gateway_id) for switch in switches) + + +class TradfriSwitch(SwitchDevice): + """The platform class required by Home Assistant.""" + + def __init__(self, switch, api, gateway_id): + """Initialize a switch.""" + self._api = api + self._unique_id = "{}-{}".format(gateway_id, switch.id) + self._switch = None + self._socket_control = None + self._switch_data = None + self._name = None + self._available = True + self._gateway_id = gateway_id + + self._refresh(switch) + + @property + def unique_id(self): + """Return unique ID for switch.""" + return self._unique_id + + @property + def device_info(self): + """Return the device info.""" + info = self._switch.device_info + + return { + 'identifiers': { + (TRADFRI_DOMAIN, self._switch.id) + }, + 'name': self._name, + 'manufacturer': info.manufacturer, + 'model': info.model_number, + 'sw_version': info.firmware_version, + 'via_hub': (TRADFRI_DOMAIN, self._gateway_id), + } + + async def async_added_to_hass(self): + """Start thread when added to hass.""" + self._async_start_observe() + + @property + def available(self): + """Return True if entity is available.""" + return self._available + + @property + def should_poll(self): + """No polling needed for tradfri switch.""" + return False + + @property + def name(self): + """Return the display name of this switch.""" + return self._name + + @property + def is_on(self): + """Return true if switch is on.""" + return self._switch_data.state + + async def async_turn_off(self, **kwargs): + """Instruct the switch to turn off.""" + await self._api(self._socket_control.set_state(False)) + + async def async_turn_on(self, **kwargs): + """Instruct the switch to turn on.""" + await self._api(self._socket_control.set_state(True)) + + @callback + def _async_start_observe(self, exc=None): + """Start observation of switch.""" + from pytradfri.error import PytradfriError + if exc: + _LOGGER.warning("Observation failed for %s", self._name, + exc_info=exc) + + try: + cmd = self._switch.observe(callback=self._observe_update, + err_callback=self._async_start_observe, + duration=0) + self.hass.async_create_task(self._api(cmd)) + except PytradfriError as err: + _LOGGER.warning("Observation failed, trying again", exc_info=err) + self._async_start_observe() + + def _refresh(self, switch): + """Refresh the switch data.""" + self._switch = switch + + # Caching of switchControl and switch object + self._available = switch.reachable + self._socket_control = switch.socket_control + self._switch_data = switch.socket_control.sockets[0] + self._name = switch.name + + @callback + def _observe_update(self, tradfri_device): + """Receive new state data for this switch.""" + self._refresh(tradfri_device) + self.async_schedule_update_ha_state() diff --git a/homeassistant/components/tradfri/__init__.py b/homeassistant/components/tradfri/__init__.py index 6e91ab338a3..51195d0a168 100644 --- a/homeassistant/components/tradfri/__init__.py +++ b/homeassistant/components/tradfri/__init__.py @@ -17,7 +17,7 @@ from .const import ( from . import config_flow # noqa pylint_disable=unused-import -REQUIREMENTS = ['pytradfri[async]==5.5.1'] +REQUIREMENTS = ['pytradfri[async]==5.6.0'] DOMAIN = 'tradfri' CONFIG_FILE = '.tradfri_psk.conf' @@ -119,5 +119,8 @@ async def async_setup_entry(hass, entry): hass.async_create_task(hass.config_entries.async_forward_entry_setup( entry, 'sensor' )) + hass.async_create_task(hass.config_entries.async_forward_entry_setup( + entry, 'switch' + )) return True diff --git a/requirements_all.txt b/requirements_all.txt index 638599b9642..c56425a9671 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1214,7 +1214,7 @@ pytouchline==0.7 pytrackr==0.0.5 # homeassistant.components.tradfri -pytradfri[async]==5.5.1 +pytradfri[async]==5.6.0 # homeassistant.components.sensor.trafikverket_weatherstation pytrafikverket==0.1.5.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 540a200ad32..7c3ace8e8cf 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -189,7 +189,7 @@ python-nest==4.0.3 pythonwhois==2.4.3 # homeassistant.components.tradfri -pytradfri[async]==5.5.1 +pytradfri[async]==5.6.0 # homeassistant.components.device_tracker.unifi pyunifi==2.13 From 750c96709e7dae579083b7a6258f0b443e70dbe7 Mon Sep 17 00:00:00 2001 From: sander76 Date: Sun, 30 Sep 2018 22:39:25 +0200 Subject: [PATCH 115/247] Homematic cloud device update fix (#17001) --- homeassistant/components/homematicip_cloud/device.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/homematicip_cloud/device.py b/homeassistant/components/homematicip_cloud/device.py index 6a4f958387e..c43f0e24e2b 100644 --- a/homeassistant/components/homematicip_cloud/device.py +++ b/homeassistant/components/homematicip_cloud/device.py @@ -36,7 +36,7 @@ class HomematicipGenericDevice(Entity): """Register callbacks.""" self._device.on_update(self._device_changed) - def _device_changed(self, json, **kwargs): + def _device_changed(self, *args, **kwargs): """Handle device state changes.""" _LOGGER.debug("Event %s (%s)", self.name, self._device.modelType) self.async_schedule_update_ha_state() From 38e371c5d9dd6dda9305f86772668581731ef7f9 Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Mon, 1 Oct 2018 08:49:19 +0200 Subject: [PATCH 116/247] Async syntax 1, alarm_control_panel & automation & binary_sensor (#17015) --- .../alarm_control_panel/__init__.py | 6 ++-- .../alarm_control_panel/alarmdecoder.py | 4 +-- .../alarm_control_panel/alarmdotcom.py | 33 ++++++++----------- .../components/alarm_control_panel/egardia.py | 4 +-- .../alarm_control_panel/envisalink.py | 21 ++++-------- .../alarm_control_panel/manual_mqtt.py | 5 ++- .../components/alarm_control_panel/mqtt.py | 19 ++++------- .../alarm_control_panel/satel_integra.py | 24 +++++--------- .../components/alarm_control_panel/wink.py | 4 +-- homeassistant/components/automation/event.py | 4 +-- .../components/automation/homeassistant.py | 4 +-- .../components/automation/litejet.py | 4 +-- homeassistant/components/automation/mqtt.py | 6 ++-- .../components/automation/numeric_state.py | 4 +-- homeassistant/components/automation/state.py | 4 +-- homeassistant/components/automation/sun.py | 4 +-- .../components/automation/template.py | 4 +-- homeassistant/components/automation/time.py | 4 +-- homeassistant/components/automation/zone.py | 4 +-- homeassistant/components/binary_sensor/ads.py | 4 +-- .../components/binary_sensor/alarmdecoder.py | 4 +-- .../binary_sensor/android_ip_webcam.py | 10 ++---- .../components/binary_sensor/bayesian.py | 12 +++---- .../binary_sensor/bmw_connected_drive.py | 4 +-- .../components/binary_sensor/egardia.py | 6 ++-- .../components/binary_sensor/envisalink.py | 9 ++--- .../components/binary_sensor/ffmpeg_motion.py | 11 +++---- .../components/binary_sensor/ffmpeg_noise.py | 11 +++---- .../components/binary_sensor/insteon.py | 6 ++-- .../components/binary_sensor/isy994.py | 12 +++---- .../components/binary_sensor/mqtt.py | 10 +++--- .../components/binary_sensor/mychevy.py | 10 ++---- .../components/binary_sensor/mystrom.py | 14 +++----- .../components/binary_sensor/satel_integra.py | 9 ++--- .../components/binary_sensor/template.py | 9 ++--- .../components/binary_sensor/threshold.py | 9 ++--- .../components/binary_sensor/trend.py | 9 ++--- .../components/binary_sensor/wink.py | 4 +-- .../components/binary_sensor/workday.py | 4 +-- 39 files changed, 109 insertions(+), 220 deletions(-) diff --git a/homeassistant/components/alarm_control_panel/__init__.py b/homeassistant/components/alarm_control_panel/__init__.py index c5110a2ad5a..a42e6e880b5 100644 --- a/homeassistant/components/alarm_control_panel/__init__.py +++ b/homeassistant/components/alarm_control_panel/__init__.py @@ -4,7 +4,6 @@ Component to interface with an alarm control panel. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/alarm_control_panel/ """ -import asyncio from datetime import timedelta import logging @@ -31,13 +30,12 @@ ALARM_SERVICE_SCHEMA = vol.Schema({ }) -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Track states and offer events for sensors.""" component = hass.data[DOMAIN] = EntityComponent( logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL) - yield from component.async_setup(config) + await component.async_setup(config) component.async_register_entity_service( SERVICE_ALARM_DISARM, ALARM_SERVICE_SCHEMA, diff --git a/homeassistant/components/alarm_control_panel/alarmdecoder.py b/homeassistant/components/alarm_control_panel/alarmdecoder.py index 5606209d1e6..25496dff0eb 100644 --- a/homeassistant/components/alarm_control_panel/alarmdecoder.py +++ b/homeassistant/components/alarm_control_panel/alarmdecoder.py @@ -4,7 +4,6 @@ Support for AlarmDecoder-based alarm control panels (Honeywell/DSC). For more details about this platform, please refer to the documentation at https://home-assistant.io/components/alarm_control_panel.alarmdecoder/ """ -import asyncio import logging import voluptuous as vol @@ -59,8 +58,7 @@ class AlarmDecoderAlarmPanel(alarm.AlarmControlPanel): self._ready = None self._zone_bypassed = None - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Register callbacks.""" self.hass.helpers.dispatcher.async_dispatcher_connect( SIGNAL_PANEL_MESSAGE, self._message_callback) diff --git a/homeassistant/components/alarm_control_panel/alarmdotcom.py b/homeassistant/components/alarm_control_panel/alarmdotcom.py index 98766deb3b6..9b07dc41690 100644 --- a/homeassistant/components/alarm_control_panel/alarmdotcom.py +++ b/homeassistant/components/alarm_control_panel/alarmdotcom.py @@ -4,7 +4,6 @@ Interfaces with Alarm.com alarm control panels. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/alarm_control_panel.alarmdotcom/ """ -import asyncio import logging import re @@ -32,9 +31,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up a Alarm.com control panel.""" name = config.get(CONF_NAME) code = config.get(CONF_CODE) @@ -42,7 +40,7 @@ def async_setup_platform(hass, config, async_add_entities, password = config.get(CONF_PASSWORD) alarmdotcom = AlarmDotCom(hass, name, code, username, password) - yield from alarmdotcom.async_login() + await alarmdotcom.async_login() async_add_entities([alarmdotcom]) @@ -63,15 +61,13 @@ class AlarmDotCom(alarm.AlarmControlPanel): self._alarm = Alarmdotcom( username, password, self._websession, hass.loop) - @asyncio.coroutine - def async_login(self): + async def async_login(self): """Login to Alarm.com.""" - yield from self._alarm.async_login() + await self._alarm.async_login() - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Fetch the latest state.""" - yield from self._alarm.async_update() + await self._alarm.async_update() return self._alarm.state @property @@ -106,23 +102,20 @@ class AlarmDotCom(alarm.AlarmControlPanel): 'sensor_status': self._alarm.sensor_status } - @asyncio.coroutine - def async_alarm_disarm(self, code=None): + async def async_alarm_disarm(self, code=None): """Send disarm command.""" if self._validate_code(code): - yield from self._alarm.async_alarm_disarm() + await self._alarm.async_alarm_disarm() - @asyncio.coroutine - def async_alarm_arm_home(self, code=None): + async def async_alarm_arm_home(self, code=None): """Send arm hom command.""" if self._validate_code(code): - yield from self._alarm.async_alarm_arm_home() + await self._alarm.async_alarm_arm_home() - @asyncio.coroutine - def async_alarm_arm_away(self, code=None): + async def async_alarm_arm_away(self, code=None): """Send arm away command.""" if self._validate_code(code): - yield from self._alarm.async_alarm_arm_away() + await self._alarm.async_alarm_arm_away() def _validate_code(self, code): """Validate given code.""" diff --git a/homeassistant/components/alarm_control_panel/egardia.py b/homeassistant/components/alarm_control_panel/egardia.py index 4e278c10e07..dfd60c4abde 100644 --- a/homeassistant/components/alarm_control_panel/egardia.py +++ b/homeassistant/components/alarm_control_panel/egardia.py @@ -4,7 +4,6 @@ Interfaces with Egardia/Woonveilig alarm control panel. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/alarm_control_panel.egardia/ """ -import asyncio import logging import requests @@ -61,8 +60,7 @@ class EgardiaAlarm(alarm.AlarmControlPanel): self._rs_codes = rs_codes self._rs_port = rs_port - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Add Egardiaserver callback if enabled.""" if self._rs_enabled: _LOGGER.debug("Registering callback to Egardiaserver") diff --git a/homeassistant/components/alarm_control_panel/envisalink.py b/homeassistant/components/alarm_control_panel/envisalink.py index df91884b32c..f0f3d2a43f7 100644 --- a/homeassistant/components/alarm_control_panel/envisalink.py +++ b/homeassistant/components/alarm_control_panel/envisalink.py @@ -4,7 +4,6 @@ Support for Envisalink-based alarm control panels (Honeywell/DSC). For more details about this platform, please refer to the documentation at https://home-assistant.io/components/alarm_control_panel.envisalink/ """ -import asyncio import logging import voluptuous as vol @@ -32,9 +31,8 @@ ALARM_KEYPRESS_SCHEMA = vol.Schema({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Perform the setup for Envisalink alarm panels.""" configured_partitions = discovery_info['partitions'] code = discovery_info[CONF_CODE] @@ -88,8 +86,7 @@ class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel): _LOGGER.debug("Setting up alarm: %s", alarm_name) super().__init__(alarm_name, info, controller) - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Register callbacks.""" async_dispatcher_connect( self.hass, SIGNAL_KEYPAD_UPDATE, self._update_callback) @@ -128,8 +125,7 @@ class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel): state = STATE_ALARM_DISARMED return state - @asyncio.coroutine - def async_alarm_disarm(self, code=None): + async def async_alarm_disarm(self, code=None): """Send disarm command.""" if code: self.hass.data[DATA_EVL].disarm_partition( @@ -138,8 +134,7 @@ class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel): self.hass.data[DATA_EVL].disarm_partition( str(self._code), self._partition_number) - @asyncio.coroutine - def async_alarm_arm_home(self, code=None): + async def async_alarm_arm_home(self, code=None): """Send arm home command.""" if code: self.hass.data[DATA_EVL].arm_stay_partition( @@ -148,8 +143,7 @@ class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel): self.hass.data[DATA_EVL].arm_stay_partition( str(self._code), self._partition_number) - @asyncio.coroutine - def async_alarm_arm_away(self, code=None): + async def async_alarm_arm_away(self, code=None): """Send arm away command.""" if code: self.hass.data[DATA_EVL].arm_away_partition( @@ -158,8 +152,7 @@ class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel): self.hass.data[DATA_EVL].arm_away_partition( str(self._code), self._partition_number) - @asyncio.coroutine - def async_alarm_trigger(self, code=None): + async def async_alarm_trigger(self, code=None): """Alarm trigger command. Will be used to trigger a panic alarm.""" self.hass.data[DATA_EVL].panic_alarm(self._panic_type) diff --git a/homeassistant/components/alarm_control_panel/manual_mqtt.py b/homeassistant/components/alarm_control_panel/manual_mqtt.py index 7bf9443424c..834a502baa0 100644 --- a/homeassistant/components/alarm_control_panel/manual_mqtt.py +++ b/homeassistant/components/alarm_control_panel/manual_mqtt.py @@ -4,7 +4,6 @@ Support for manual alarms controllable via MQTT. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/alarm_control_panel.manual_mqtt/ """ -import asyncio import copy import datetime import logging @@ -363,8 +362,8 @@ class ManualMQTTAlarm(alarm.AlarmControlPanel): return mqtt.async_subscribe( self.hass, self._command_topic, message_received, self._qos) - @asyncio.coroutine - def _async_state_changed_listener(self, entity_id, old_state, new_state): + async def _async_state_changed_listener(self, entity_id, old_state, + new_state): """Publish state change to MQTT.""" mqtt.async_publish( self.hass, self._state_topic, new_state.state, self._qos, True) diff --git a/homeassistant/components/alarm_control_panel/mqtt.py b/homeassistant/components/alarm_control_panel/mqtt.py index d8193f958da..ad1c0d1e3b8 100644 --- a/homeassistant/components/alarm_control_panel/mqtt.py +++ b/homeassistant/components/alarm_control_panel/mqtt.py @@ -4,7 +4,6 @@ This platform enables the possibility to control a MQTT alarm. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/alarm_control_panel.mqtt/ """ -import asyncio import logging import re @@ -111,11 +110,10 @@ class MqttAlarm(MqttAvailability, MqttDiscoveryUpdate, self._code = code self._discovery_hash = discovery_hash - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Subscribe mqtt events.""" - yield from MqttAvailability.async_added_to_hass(self) - yield from MqttDiscoveryUpdate.async_added_to_hass(self) + await MqttAvailability.async_added_to_hass(self) + await MqttDiscoveryUpdate.async_added_to_hass(self) @callback def message_received(topic, payload, qos): @@ -128,7 +126,7 @@ class MqttAlarm(MqttAvailability, MqttDiscoveryUpdate, self._state = payload self.async_schedule_update_ha_state() - yield from mqtt.async_subscribe( + await mqtt.async_subscribe( self.hass, self._state_topic, message_received, self._qos) @property @@ -155,8 +153,7 @@ class MqttAlarm(MqttAvailability, MqttDiscoveryUpdate, return 'Number' return 'Any' - @asyncio.coroutine - def async_alarm_disarm(self, code=None): + async def async_alarm_disarm(self, code=None): """Send disarm command. This method is a coroutine. @@ -167,8 +164,7 @@ class MqttAlarm(MqttAvailability, MqttDiscoveryUpdate, self.hass, self._command_topic, self._payload_disarm, self._qos, self._retain) - @asyncio.coroutine - def async_alarm_arm_home(self, code=None): + async def async_alarm_arm_home(self, code=None): """Send arm home command. This method is a coroutine. @@ -179,8 +175,7 @@ class MqttAlarm(MqttAvailability, MqttDiscoveryUpdate, self.hass, self._command_topic, self._payload_arm_home, self._qos, self._retain) - @asyncio.coroutine - def async_alarm_arm_away(self, code=None): + async def async_alarm_arm_away(self, code=None): """Send arm away command. This method is a coroutine. diff --git a/homeassistant/components/alarm_control_panel/satel_integra.py b/homeassistant/components/alarm_control_panel/satel_integra.py index 86603763396..c4e42855d8a 100644 --- a/homeassistant/components/alarm_control_panel/satel_integra.py +++ b/homeassistant/components/alarm_control_panel/satel_integra.py @@ -4,7 +4,6 @@ Support for Satel Integra alarm, using ETHM module: https://www.satel.pl/en/ . For more details about this platform, please refer to the documentation at https://home-assistant.io/components/alarm_control_panel.satel_integra/ """ -import asyncio import logging import homeassistant.components.alarm_control_panel as alarm @@ -18,9 +17,8 @@ _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['satel_integra'] -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up for Satel Integra alarm panels.""" if not discovery_info: return @@ -39,8 +37,7 @@ class SatelIntegraAlarmPanel(alarm.AlarmControlPanel): self._state = None self._arm_home_mode = arm_home_mode - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Register callbacks.""" async_dispatcher_connect( self.hass, SIGNAL_PANEL_MESSAGE, self._message_callback) @@ -74,21 +71,18 @@ class SatelIntegraAlarmPanel(alarm.AlarmControlPanel): """Return the state of the device.""" return self._state - @asyncio.coroutine - def async_alarm_disarm(self, code=None): + async def async_alarm_disarm(self, code=None): """Send disarm command.""" if code: - yield from self.hass.data[DATA_SATEL].disarm(code) + await self.hass.data[DATA_SATEL].disarm(code) - @asyncio.coroutine - def async_alarm_arm_away(self, code=None): + async def async_alarm_arm_away(self, code=None): """Send arm away command.""" if code: - yield from self.hass.data[DATA_SATEL].arm(code) + await self.hass.data[DATA_SATEL].arm(code) - @asyncio.coroutine - def async_alarm_arm_home(self, code=None): + async def async_alarm_arm_home(self, code=None): """Send arm home command.""" if code: - yield from self.hass.data[DATA_SATEL].arm( + await self.hass.data[DATA_SATEL].arm( code, self._arm_home_mode) diff --git a/homeassistant/components/alarm_control_panel/wink.py b/homeassistant/components/alarm_control_panel/wink.py index d75fad30c96..001c6fad85c 100644 --- a/homeassistant/components/alarm_control_panel/wink.py +++ b/homeassistant/components/alarm_control_panel/wink.py @@ -4,7 +4,6 @@ Interfaces with Wink Cameras. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/alarm_control_panel.wink/ """ -import asyncio import logging import homeassistant.components.alarm_control_panel as alarm @@ -38,8 +37,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class WinkCameraDevice(WinkDevice, alarm.AlarmControlPanel): """Representation a Wink camera alarm.""" - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Call when entity is added to hass.""" self.hass.data[DOMAIN]['entities']['alarm_control_panel'].append(self) diff --git a/homeassistant/components/automation/event.py b/homeassistant/components/automation/event.py index e19a85edae6..a9605f343fd 100644 --- a/homeassistant/components/automation/event.py +++ b/homeassistant/components/automation/event.py @@ -4,7 +4,6 @@ Offer event listening automation rules. For more details about this automation rule, please refer to the documentation at https://home-assistant.io/docs/automation/trigger/#event-trigger """ -import asyncio import logging import voluptuous as vol @@ -25,8 +24,7 @@ TRIGGER_SCHEMA = vol.Schema({ }) -@asyncio.coroutine -def async_trigger(hass, config, action): +async def async_trigger(hass, config, action): """Listen for events based on configuration.""" event_type = config.get(CONF_EVENT_TYPE) event_data_schema = vol.Schema( diff --git a/homeassistant/components/automation/homeassistant.py b/homeassistant/components/automation/homeassistant.py index b55d99f706a..30ab979d6f4 100644 --- a/homeassistant/components/automation/homeassistant.py +++ b/homeassistant/components/automation/homeassistant.py @@ -4,7 +4,6 @@ Offer Home Assistant core automation rules. For more details about this automation rule, please refer to the documentation at https://home-assistant.io/components/automation/#homeassistant-trigger """ -import asyncio import logging import voluptuous as vol @@ -23,8 +22,7 @@ TRIGGER_SCHEMA = vol.Schema({ }) -@asyncio.coroutine -def async_trigger(hass, config, action): +async def async_trigger(hass, config, action): """Listen for events based on configuration.""" event = config.get(CONF_EVENT) diff --git a/homeassistant/components/automation/litejet.py b/homeassistant/components/automation/litejet.py index c827fe8f7a4..c0d2dd99ba2 100644 --- a/homeassistant/components/automation/litejet.py +++ b/homeassistant/components/automation/litejet.py @@ -4,7 +4,6 @@ Trigger an automation when a LiteJet switch is released. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/automation.litejet/ """ -import asyncio import logging import voluptuous as vol @@ -33,8 +32,7 @@ TRIGGER_SCHEMA = vol.Schema({ }) -@asyncio.coroutine -def async_trigger(hass, config, action): +async def async_trigger(hass, config, action): """Listen for events based on configuration.""" number = config.get(CONF_NUMBER) held_more_than = config.get(CONF_HELD_MORE_THAN) diff --git a/homeassistant/components/automation/mqtt.py b/homeassistant/components/automation/mqtt.py index 60c33ca9b0e..99d5ab8674c 100644 --- a/homeassistant/components/automation/mqtt.py +++ b/homeassistant/components/automation/mqtt.py @@ -4,7 +4,6 @@ Offer MQTT listening automation rules. For more details about this automation rule, please refer to the documentation at https://home-assistant.io/docs/automation/trigger/#mqtt-trigger """ -import asyncio import json import voluptuous as vol @@ -25,8 +24,7 @@ TRIGGER_SCHEMA = vol.Schema({ }) -@asyncio.coroutine -def async_trigger(hass, config, action): +async def async_trigger(hass, config, action): """Listen for state changes based on configuration.""" topic = config.get(CONF_TOPIC) payload = config.get(CONF_PAYLOAD) @@ -51,6 +49,6 @@ def async_trigger(hass, config, action): 'trigger': data }) - remove = yield from mqtt.async_subscribe( + remove = await mqtt.async_subscribe( hass, topic, mqtt_automation_listener) return remove diff --git a/homeassistant/components/automation/numeric_state.py b/homeassistant/components/automation/numeric_state.py index f0dcbf0be57..675b6f3653a 100644 --- a/homeassistant/components/automation/numeric_state.py +++ b/homeassistant/components/automation/numeric_state.py @@ -4,7 +4,6 @@ Offer numeric state listening automation rules. For more details about this automation rule, please refer to the documentation at https://home-assistant.io/docs/automation/trigger/#numeric-state-trigger """ -import asyncio import logging import voluptuous as vol @@ -29,8 +28,7 @@ TRIGGER_SCHEMA = vol.All(vol.Schema({ _LOGGER = logging.getLogger(__name__) -@asyncio.coroutine -def async_trigger(hass, config, action): +async def async_trigger(hass, config, action): """Listen for state changes based on configuration.""" entity_id = config.get(CONF_ENTITY_ID) below = config.get(CONF_BELOW) diff --git a/homeassistant/components/automation/state.py b/homeassistant/components/automation/state.py index 263d4158e25..46c5cafa071 100644 --- a/homeassistant/components/automation/state.py +++ b/homeassistant/components/automation/state.py @@ -4,7 +4,6 @@ Offer state listening automation rules. For more details about this automation rule, please refer to the documentation at https://home-assistant.io/docs/automation/trigger/#state-trigger """ -import asyncio import voluptuous as vol from homeassistant.core import callback @@ -27,8 +26,7 @@ TRIGGER_SCHEMA = vol.All(vol.Schema({ }), cv.key_dependency(CONF_FOR, CONF_TO)) -@asyncio.coroutine -def async_trigger(hass, config, action): +async def async_trigger(hass, config, action): """Listen for state changes based on configuration.""" entity_id = config.get(CONF_ENTITY_ID) from_state = config.get(CONF_FROM, MATCH_ALL) diff --git a/homeassistant/components/automation/sun.py b/homeassistant/components/automation/sun.py index 497b8453267..7cefe6953a1 100644 --- a/homeassistant/components/automation/sun.py +++ b/homeassistant/components/automation/sun.py @@ -4,7 +4,6 @@ Offer sun based automation rules. For more details about this automation rule, please refer to the documentation at https://home-assistant.io/docs/automation/trigger/#sun-trigger """ -import asyncio from datetime import timedelta import logging @@ -25,8 +24,7 @@ TRIGGER_SCHEMA = vol.Schema({ }) -@asyncio.coroutine -def async_trigger(hass, config, action): +async def async_trigger(hass, config, action): """Listen for events based on configuration.""" event = config.get(CONF_EVENT) offset = config.get(CONF_OFFSET) diff --git a/homeassistant/components/automation/template.py b/homeassistant/components/automation/template.py index 67a44f1a347..c0d83b1067f 100644 --- a/homeassistant/components/automation/template.py +++ b/homeassistant/components/automation/template.py @@ -4,7 +4,6 @@ Offer template automation rules. For more details about this automation rule, please refer to the documentation at https://home-assistant.io/docs/automation/trigger/#template-trigger """ -import asyncio import logging import voluptuous as vol @@ -23,8 +22,7 @@ TRIGGER_SCHEMA = IF_ACTION_SCHEMA = vol.Schema({ }) -@asyncio.coroutine -def async_trigger(hass, config, action): +async def async_trigger(hass, config, action): """Listen for state changes based on configuration.""" value_template = config.get(CONF_VALUE_TEMPLATE) value_template.hass = hass diff --git a/homeassistant/components/automation/time.py b/homeassistant/components/automation/time.py index a3a8496c3c5..eccc31581a0 100644 --- a/homeassistant/components/automation/time.py +++ b/homeassistant/components/automation/time.py @@ -4,7 +4,6 @@ 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 asyncio import logging import voluptuous as vol @@ -29,8 +28,7 @@ TRIGGER_SCHEMA = vol.All(vol.Schema({ }), cv.has_at_least_one_key(CONF_HOURS, CONF_MINUTES, CONF_SECONDS, CONF_AT)) -@asyncio.coroutine -def async_trigger(hass, config, action): +async def async_trigger(hass, config, action): """Listen for state changes based on configuration.""" if CONF_AT in config: at_time = config.get(CONF_AT) diff --git a/homeassistant/components/automation/zone.py b/homeassistant/components/automation/zone.py index f30dfe753cb..dfc9cc418bf 100644 --- a/homeassistant/components/automation/zone.py +++ b/homeassistant/components/automation/zone.py @@ -4,7 +4,6 @@ Offer zone automation rules. For more details about this automation rule, please refer to the documentation at https://home-assistant.io/docs/automation/trigger/#zone-trigger """ -import asyncio import voluptuous as vol from homeassistant.core import callback @@ -27,8 +26,7 @@ TRIGGER_SCHEMA = vol.Schema({ }) -@asyncio.coroutine -def async_trigger(hass, config, action): +async def async_trigger(hass, config, action): """Listen for state changes based on configuration.""" entity_id = config.get(CONF_ENTITY_ID) zone_entity_id = config.get(CONF_ZONE) diff --git a/homeassistant/components/binary_sensor/ads.py b/homeassistant/components/binary_sensor/ads.py index d46ff5ec2ee..1ee56cac9d3 100644 --- a/homeassistant/components/binary_sensor/ads.py +++ b/homeassistant/components/binary_sensor/ads.py @@ -4,7 +4,6 @@ Support for ADS binary sensors. For more details about this platform, please refer to the documentation. https://home-assistant.io/components/binary_sensor.ads/ """ -import asyncio import logging import voluptuous as vol @@ -50,8 +49,7 @@ class AdsBinarySensor(BinarySensorDevice): self._ads_hub = ads_hub self.ads_var = ads_var - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Register device notification.""" def update(name, value): """Handle device notifications.""" diff --git a/homeassistant/components/binary_sensor/alarmdecoder.py b/homeassistant/components/binary_sensor/alarmdecoder.py index 82bcc50259f..1b50d6c6c72 100644 --- a/homeassistant/components/binary_sensor/alarmdecoder.py +++ b/homeassistant/components/binary_sensor/alarmdecoder.py @@ -4,7 +4,6 @@ Support for AlarmDecoder zone states- represented as binary sensors. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/binary_sensor.alarmdecoder/ """ -import asyncio import logging from homeassistant.components.binary_sensor import BinarySensorDevice @@ -64,8 +63,7 @@ class AlarmDecoderBinarySensor(BinarySensorDevice): self._relay_addr = relay_addr self._relay_chan = relay_chan - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Register callbacks.""" self.hass.helpers.dispatcher.async_dispatcher_connect( SIGNAL_ZONE_FAULT, self._fault_callback) diff --git a/homeassistant/components/binary_sensor/android_ip_webcam.py b/homeassistant/components/binary_sensor/android_ip_webcam.py index 58de81c30e7..085bafd3ae3 100644 --- a/homeassistant/components/binary_sensor/android_ip_webcam.py +++ b/homeassistant/components/binary_sensor/android_ip_webcam.py @@ -4,8 +4,6 @@ Support for IP Webcam binary sensors. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/binary_sensor.android_ip_webcam/ """ -import asyncio - from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.components.android_ip_webcam import ( KEY_MAP, DATA_IP_WEBCAM, AndroidIPCamEntity, CONF_HOST, CONF_NAME) @@ -13,9 +11,8 @@ from homeassistant.components.android_ip_webcam import ( DEPENDENCIES = ['android_ip_webcam'] -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the IP Webcam binary sensors.""" if discovery_info is None: return @@ -51,8 +48,7 @@ class IPWebcamBinarySensor(AndroidIPCamEntity, BinarySensorDevice): """Return true if the binary sensor is on.""" return self._state - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Retrieve latest state.""" state, _ = self._ipcam.export_sensor(self._sensor) self._state = state == 1.0 diff --git a/homeassistant/components/binary_sensor/bayesian.py b/homeassistant/components/binary_sensor/bayesian.py index 88669d67d80..f7802f0f29d 100644 --- a/homeassistant/components/binary_sensor/bayesian.py +++ b/homeassistant/components/binary_sensor/bayesian.py @@ -4,7 +4,6 @@ Use Bayesian Inference to trigger a binary sensor. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/binary_sensor.bayesian/ """ -import asyncio import logging from collections import OrderedDict @@ -74,9 +73,8 @@ def update_probability(prior, prob_true, prob_false): return probability -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the Bayesian Binary sensor.""" name = config.get(CONF_NAME) observations = config.get(CONF_OBSERVATIONS) @@ -119,8 +117,7 @@ class BayesianBinarySensor(BinarySensorDevice): 'state': self._process_state } - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Call when entity about to be added.""" @callback def async_threshold_sensor_state_listener(entity, old_state, @@ -214,7 +211,6 @@ class BayesianBinarySensor(BinarySensorDevice): ATTR_PROBABILITY_THRESHOLD: self._probability_threshold, } - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Get the latest data and update the states.""" self._deviation = bool(self.probability >= self._probability_threshold) diff --git a/homeassistant/components/binary_sensor/bmw_connected_drive.py b/homeassistant/components/binary_sensor/bmw_connected_drive.py index e3b1a941bd2..3fe8136c93b 100644 --- a/homeassistant/components/binary_sensor/bmw_connected_drive.py +++ b/homeassistant/components/binary_sensor/bmw_connected_drive.py @@ -4,7 +4,6 @@ Reads vehicle status from BMW connected drive portal. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/binary_sensor.bmw_connected_drive/ """ -import asyncio import logging from homeassistant.components.binary_sensor import BinarySensorDevice @@ -193,8 +192,7 @@ class BMWConnectedDriveSensor(BinarySensorDevice): """Schedule a state update.""" self.schedule_update_ha_state(True) - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Add callback after being added to hass. Show latest data after startup. diff --git a/homeassistant/components/binary_sensor/egardia.py b/homeassistant/components/binary_sensor/egardia.py index 0db2cac667f..56d7dda17ba 100644 --- a/homeassistant/components/binary_sensor/egardia.py +++ b/homeassistant/components/binary_sensor/egardia.py @@ -4,7 +4,6 @@ Interfaces with Egardia/Woonveilig alarm control panel. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/binary_sensor.egardia/ """ -import asyncio import logging from homeassistant.components.binary_sensor import BinarySensorDevice @@ -18,9 +17,8 @@ EGARDIA_TYPE_TO_DEVICE_CLASS = {'IR Sensor': 'motion', 'IR': 'motion'} -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Initialize the platform.""" if (discovery_info is None or discovery_info[ATTR_DISCOVER_DEVICES] is None): diff --git a/homeassistant/components/binary_sensor/envisalink.py b/homeassistant/components/binary_sensor/envisalink.py index 2568879bcc6..276ace8dd51 100644 --- a/homeassistant/components/binary_sensor/envisalink.py +++ b/homeassistant/components/binary_sensor/envisalink.py @@ -4,7 +4,6 @@ Support for Envisalink zone states- represented as binary sensors. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/binary_sensor.envisalink/ """ -import asyncio import logging import datetime @@ -22,9 +21,8 @@ _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['envisalink'] -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the Envisalink binary sensor devices.""" configured_zones = discovery_info['zones'] @@ -56,8 +54,7 @@ class EnvisalinkBinarySensor(EnvisalinkDevice, BinarySensorDevice): _LOGGER.debug('Setting up zone: %s', zone_name) super().__init__(zone_name, info, controller) - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Register callbacks.""" async_dispatcher_connect( self.hass, SIGNAL_ZONE_UPDATE, self._update_callback) diff --git a/homeassistant/components/binary_sensor/ffmpeg_motion.py b/homeassistant/components/binary_sensor/ffmpeg_motion.py index 365bcafbd69..899d442c14e 100644 --- a/homeassistant/components/binary_sensor/ffmpeg_motion.py +++ b/homeassistant/components/binary_sensor/ffmpeg_motion.py @@ -4,7 +4,6 @@ Provides a binary sensor which is a collection of ffmpeg tools. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/binary_sensor.ffmpeg_motion/ """ -import asyncio import logging import voluptuous as vol @@ -46,9 +45,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the FFmpeg binary motion sensor.""" manager = hass.data[DATA_FFMPEG] @@ -98,8 +96,7 @@ class FFmpegMotion(FFmpegBinarySensor): self.ffmpeg = SensorMotion( manager.binary, hass.loop, self._async_callback) - @asyncio.coroutine - def _async_start_ffmpeg(self, entity_ids): + async def _async_start_ffmpeg(self, entity_ids): """Start a FFmpeg instance. This method is a coroutine. @@ -116,7 +113,7 @@ class FFmpegMotion(FFmpegBinarySensor): ) # run - yield from self.ffmpeg.open_sensor( + await self.ffmpeg.open_sensor( input_source=self._config.get(CONF_INPUT), extra_cmd=self._config.get(CONF_EXTRA_ARGUMENTS), ) diff --git a/homeassistant/components/binary_sensor/ffmpeg_noise.py b/homeassistant/components/binary_sensor/ffmpeg_noise.py index 73c84ac336d..60eb236767a 100644 --- a/homeassistant/components/binary_sensor/ffmpeg_noise.py +++ b/homeassistant/components/binary_sensor/ffmpeg_noise.py @@ -4,7 +4,6 @@ Provides a binary sensor which is a collection of ffmpeg tools. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/binary_sensor.ffmpeg_noise/ """ -import asyncio import logging import voluptuous as vol @@ -43,9 +42,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the FFmpeg noise binary sensor.""" manager = hass.data[DATA_FFMPEG] @@ -67,8 +65,7 @@ class FFmpegNoise(FFmpegBinarySensor): self.ffmpeg = SensorNoise( manager.binary, hass.loop, self._async_callback) - @asyncio.coroutine - def _async_start_ffmpeg(self, entity_ids): + async def _async_start_ffmpeg(self, entity_ids): """Start a FFmpeg instance. This method is a coroutine. @@ -82,7 +79,7 @@ class FFmpegNoise(FFmpegBinarySensor): peak=self._config.get(CONF_PEAK), ) - yield from self.ffmpeg.open_sensor( + await self.ffmpeg.open_sensor( input_source=self._config.get(CONF_INPUT), output_dest=self._config.get(CONF_OUTPUT), extra_cmd=self._config.get(CONF_EXTRA_ARGUMENTS), diff --git a/homeassistant/components/binary_sensor/insteon.py b/homeassistant/components/binary_sensor/insteon.py index 533ff2d76c0..c399d31a95b 100644 --- a/homeassistant/components/binary_sensor/insteon.py +++ b/homeassistant/components/binary_sensor/insteon.py @@ -4,7 +4,6 @@ Support for INSTEON dimmers via PowerLinc Modem. For more details about this component, please refer to the documentation at https://home-assistant.io/components/binary_sensor.insteon/ """ -import asyncio import logging from homeassistant.components.binary_sensor import BinarySensorDevice @@ -22,9 +21,8 @@ SENSOR_TYPES = {'openClosedSensor': 'opening', 'batterySensor': 'battery'} -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the INSTEON device class for the hass platform.""" insteon_modem = hass.data['insteon'].get('modem') diff --git a/homeassistant/components/binary_sensor/isy994.py b/homeassistant/components/binary_sensor/isy994.py index 36dacb06738..31a9606d950 100644 --- a/homeassistant/components/binary_sensor/isy994.py +++ b/homeassistant/components/binary_sensor/isy994.py @@ -4,8 +4,6 @@ Support for ISY994 binary sensors. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/binary_sensor.isy994/ """ - -import asyncio import logging from datetime import timedelta from typing import Callable @@ -121,10 +119,9 @@ class ISYBinarySensorDevice(ISYDevice, BinarySensorDevice): self._computed_state = bool(self._node.status._val) self._status_was_unknown = False - @asyncio.coroutine - def async_added_to_hass(self) -> None: + async def async_added_to_hass(self) -> None: """Subscribe to the node and subnode event emitters.""" - yield from super().async_added_to_hass() + await super().async_added_to_hass() self._node.controlEvents.subscribe(self._positive_node_control_handler) @@ -261,10 +258,9 @@ class ISYBinarySensorHeartbeat(ISYDevice, BinarySensorDevice): self._parent_device = parent_device self._heartbeat_timer = None - @asyncio.coroutine - def async_added_to_hass(self) -> None: + async def async_added_to_hass(self) -> None: """Subscribe to the node and subnode event emitters.""" - yield from super().async_added_to_hass() + await super().async_added_to_hass() self._node.controlEvents.subscribe( self._heartbeat_node_control_handler) diff --git a/homeassistant/components/binary_sensor/mqtt.py b/homeassistant/components/binary_sensor/mqtt.py index 944cea96b33..baaf6a9a567 100644 --- a/homeassistant/components/binary_sensor/mqtt.py +++ b/homeassistant/components/binary_sensor/mqtt.py @@ -4,7 +4,6 @@ Support for MQTT binary sensors. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/binary_sensor.mqtt/ """ -import asyncio import logging from typing import Optional @@ -115,11 +114,10 @@ class MqttBinarySensor(MqttAvailability, MqttDiscoveryUpdate, self._unique_id = unique_id self._discovery_hash = discovery_hash - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Subscribe mqtt events.""" - yield from MqttAvailability.async_added_to_hass(self) - yield from MqttDiscoveryUpdate.async_added_to_hass(self) + await MqttAvailability.async_added_to_hass(self) + await MqttDiscoveryUpdate.async_added_to_hass(self) @callback def state_message_received(topic, payload, qos): @@ -139,7 +137,7 @@ class MqttBinarySensor(MqttAvailability, MqttDiscoveryUpdate, self.async_schedule_update_ha_state() - yield from mqtt.async_subscribe( + await mqtt.async_subscribe( self.hass, self._state_topic, state_message_received, self._qos) @property diff --git a/homeassistant/components/binary_sensor/mychevy.py b/homeassistant/components/binary_sensor/mychevy.py index d1438379da1..c1e3b6f0aac 100644 --- a/homeassistant/components/binary_sensor/mychevy.py +++ b/homeassistant/components/binary_sensor/mychevy.py @@ -3,8 +3,6 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/binary_sensor.mychevy/ """ - -import asyncio import logging from homeassistant.components.mychevy import ( @@ -22,9 +20,8 @@ SENSORS = [ ] -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the MyChevy sensors.""" if discovery_info is None: return @@ -75,8 +72,7 @@ class EVBinarySensor(BinarySensorDevice): """Return the car.""" return self._conn.get_car(self._car_vid) - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Register callbacks.""" self.hass.helpers.dispatcher.async_dispatcher_connect( UPDATE_TOPIC, self.async_update_callback) diff --git a/homeassistant/components/binary_sensor/mystrom.py b/homeassistant/components/binary_sensor/mystrom.py index 23f40ce0a7f..5785ed464fd 100644 --- a/homeassistant/components/binary_sensor/mystrom.py +++ b/homeassistant/components/binary_sensor/mystrom.py @@ -4,7 +4,6 @@ Support for the myStrom buttons. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/binary_sensor.mystrom/ """ -import asyncio import logging from homeassistant.components.binary_sensor import DOMAIN, BinarySensorDevice @@ -16,9 +15,8 @@ _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['http'] -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up myStrom Binary Sensor.""" hass.http.register_view(MyStromView(async_add_entities)) @@ -37,14 +35,12 @@ class MyStromView(HomeAssistantView): self.buttons = {} self.add_entities = add_entities - @asyncio.coroutine - def get(self, request): + async def get(self, request): """Handle the GET request received from a myStrom button.""" - res = yield from self._handle(request.app['hass'], request.query) + res = await self._handle(request.app['hass'], request.query) return res - @asyncio.coroutine - def _handle(self, hass, data): + async def _handle(self, hass, data): """Handle requests to the myStrom endpoint.""" button_action = next(( parameter for parameter in data diff --git a/homeassistant/components/binary_sensor/satel_integra.py b/homeassistant/components/binary_sensor/satel_integra.py index 3500f0a0576..8aff02d55a7 100644 --- a/homeassistant/components/binary_sensor/satel_integra.py +++ b/homeassistant/components/binary_sensor/satel_integra.py @@ -4,7 +4,6 @@ Support for Satel Integra zone states- represented as binary sensors. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/binary_sensor.satel_integra/ """ -import asyncio import logging from homeassistant.components.binary_sensor import BinarySensorDevice @@ -20,9 +19,8 @@ DEPENDENCIES = ['satel_integra'] _LOGGER = logging.getLogger(__name__) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the Satel Integra binary sensor devices.""" if not discovery_info: return @@ -50,8 +48,7 @@ class SatelIntegraBinarySensor(BinarySensorDevice): self._zone_type = zone_type self._state = 0 - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Register callbacks.""" async_dispatcher_connect( self.hass, SIGNAL_ZONES_UPDATED, self._zones_updated) diff --git a/homeassistant/components/binary_sensor/template.py b/homeassistant/components/binary_sensor/template.py index c5bfa593022..89547dffbc9 100644 --- a/homeassistant/components/binary_sensor/template.py +++ b/homeassistant/components/binary_sensor/template.py @@ -4,7 +4,6 @@ Support for exposing a templated binary sensor. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/binary_sensor.template/ """ -import asyncio import logging import voluptuous as vol @@ -46,9 +45,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up template binary sensors.""" sensors = [] @@ -109,8 +107,7 @@ class BinarySensorTemplate(BinarySensorDevice): self._delay_on = delay_on self._delay_off = delay_off - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Register callbacks.""" @callback def template_bsensor_state_listener(entity, old_state, new_state): diff --git a/homeassistant/components/binary_sensor/threshold.py b/homeassistant/components/binary_sensor/threshold.py index fd7ead08822..0dadf3a61fd 100644 --- a/homeassistant/components/binary_sensor/threshold.py +++ b/homeassistant/components/binary_sensor/threshold.py @@ -4,7 +4,6 @@ Support for monitoring if a sensor value is below/above a threshold. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.threshold/ """ -import asyncio import logging import voluptuous as vol @@ -54,9 +53,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the Threshold sensor.""" entity_id = config.get(CONF_ENTITY_ID) name = config.get(CONF_NAME) @@ -147,8 +145,7 @@ class ThresholdSensor(BinarySensorDevice): ATTR_UPPER: self._threshold_upper, } - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Get the latest data and updates the states.""" def below(threshold): """Determine if the sensor value is below a threshold.""" diff --git a/homeassistant/components/binary_sensor/trend.py b/homeassistant/components/binary_sensor/trend.py index ae6fd5562bf..0b168e45b4d 100644 --- a/homeassistant/components/binary_sensor/trend.py +++ b/homeassistant/components/binary_sensor/trend.py @@ -4,7 +4,6 @@ A sensor that monitors trends in other components. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.trend/ """ -import asyncio from collections import deque import logging import math @@ -138,8 +137,7 @@ class SensorTrend(BinarySensorDevice): """No polling needed.""" return False - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Complete device setup after being added to hass.""" @callback def trend_sensor_state_listener(entity, old_state, new_state): @@ -160,8 +158,7 @@ class SensorTrend(BinarySensorDevice): self.hass, self._entity_id, trend_sensor_state_listener) - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Get the latest data and update the states.""" # Remove outdated samples if self._sample_duration > 0: @@ -173,7 +170,7 @@ class SensorTrend(BinarySensorDevice): return # Calculate gradient of linear trend - yield from self.hass.async_add_job(self._calculate_gradient) + await self.hass.async_add_job(self._calculate_gradient) # Update state self._state = ( diff --git a/homeassistant/components/binary_sensor/wink.py b/homeassistant/components/binary_sensor/wink.py index 1976e49f446..a950289789e 100644 --- a/homeassistant/components/binary_sensor/wink.py +++ b/homeassistant/components/binary_sensor/wink.py @@ -4,7 +4,6 @@ Support for Wink binary sensors. For more details about this platform, please refer to the documentation at at https://home-assistant.io/components/binary_sensor.wink/ """ -import asyncio import logging from homeassistant.components.binary_sensor import BinarySensorDevice @@ -101,8 +100,7 @@ class WinkBinarySensorDevice(WinkDevice, BinarySensorDevice): else: self.capability = None - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Call when entity is added to hass.""" self.hass.data[DOMAIN]['entities']['binary_sensor'].append(self) diff --git a/homeassistant/components/binary_sensor/workday.py b/homeassistant/components/binary_sensor/workday.py index 1d85d9c9a47..82b5e66629a 100644 --- a/homeassistant/components/binary_sensor/workday.py +++ b/homeassistant/components/binary_sensor/workday.py @@ -4,7 +4,6 @@ Sensor to indicate whether the current day is a workday. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/binary_sensor.workday/ """ -import asyncio import logging from datetime import datetime, timedelta @@ -162,8 +161,7 @@ class IsWorkdaySensor(BinarySensorDevice): CONF_OFFSET: self._days_offset } - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Get date and look whether it is a holiday.""" # Default is no workday self._state = False From 8444b9ba033daf1eb74d13d33a67a4a2ab7434c7 Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Mon, 1 Oct 2018 08:50:05 +0200 Subject: [PATCH 117/247] Async syntax 2, camera & climate & config (#17016) --- homeassistant/components/camera/abode.py | 6 +-- homeassistant/components/camera/amcrest.py | 19 +++---- homeassistant/components/camera/canary.py | 14 +++-- homeassistant/components/camera/doorbird.py | 12 ++--- homeassistant/components/camera/generic.py | 14 +++-- homeassistant/components/camera/mjpeg.py | 14 +++-- homeassistant/components/camera/ring.py | 14 +++-- homeassistant/components/camera/synology.py | 11 ++-- .../components/climate/generic_thermostat.py | 11 ++-- homeassistant/components/climate/mqtt.py | 53 ++++++++---------- .../components/climate/radiotherm.py | 4 +- homeassistant/components/climate/sensibo.py | 54 ++++++++----------- homeassistant/components/climate/wink.py | 4 +- homeassistant/components/config/automation.py | 4 +- .../components/config/config_entries.py | 18 +++---- homeassistant/components/config/core.py | 9 ++-- homeassistant/components/config/customize.py | 4 +- homeassistant/components/config/group.py | 9 ++-- homeassistant/components/config/hassbian.py | 15 ++---- homeassistant/components/config/script.py | 4 +- homeassistant/components/config/zwave.py | 9 ++-- homeassistant/components/configurator.py | 11 ++-- 22 files changed, 119 insertions(+), 194 deletions(-) diff --git a/homeassistant/components/camera/abode.py b/homeassistant/components/camera/abode.py index fbab1620a39..39681760d4d 100644 --- a/homeassistant/components/camera/abode.py +++ b/homeassistant/components/camera/abode.py @@ -4,7 +4,6 @@ This component provides HA camera support for Abode Security System. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/camera.abode/ """ -import asyncio import logging from datetime import timedelta @@ -51,10 +50,9 @@ class AbodeCamera(AbodeDevice, Camera): self._event = event self._response = None - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Subscribe Abode events.""" - yield from super().async_added_to_hass() + await super().async_added_to_hass() self.hass.async_add_job( self._data.abode.events.add_timeline_callback, diff --git a/homeassistant/components/camera/amcrest.py b/homeassistant/components/camera/amcrest.py index 9f4b84db2cc..55a9c2e4294 100644 --- a/homeassistant/components/camera/amcrest.py +++ b/homeassistant/components/camera/amcrest.py @@ -4,7 +4,6 @@ This component provides basic support for Amcrest IP cameras. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/camera.amcrest/ """ -import asyncio import logging from homeassistant.components.amcrest import ( @@ -21,9 +20,8 @@ DEPENDENCIES = ['amcrest', 'ffmpeg'] _LOGGER = logging.getLogger(__name__) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up an Amcrest IP Camera.""" if discovery_info is None: return @@ -57,12 +55,11 @@ class AmcrestCam(Camera): response = self._camera.snapshot(channel=self._resolution) return response.data - @asyncio.coroutine - def handle_async_mjpeg_stream(self, request): + async def handle_async_mjpeg_stream(self, request): """Return an MJPEG stream.""" # The snapshot implementation is handled by the parent class if self._stream_source == STREAM_SOURCE_LIST['snapshot']: - yield from super().handle_async_mjpeg_stream(request) + await super().handle_async_mjpeg_stream(request) return if self._stream_source == STREAM_SOURCE_LIST['mjpeg']: @@ -72,7 +69,7 @@ class AmcrestCam(Camera): stream_coro = websession.get( streaming_url, auth=self._token, timeout=TIMEOUT) - yield from async_aiohttp_proxy_web(self.hass, request, stream_coro) + await async_aiohttp_proxy_web(self.hass, request, stream_coro) else: # streaming via fmpeg @@ -80,13 +77,13 @@ class AmcrestCam(Camera): streaming_url = self._camera.rtsp_url(typeno=self._resolution) stream = CameraMjpeg(self._ffmpeg.binary, loop=self.hass.loop) - yield from stream.open_camera( + await stream.open_camera( streaming_url, extra_cmd=self._ffmpeg_arguments) - yield from async_aiohttp_proxy_stream( + await async_aiohttp_proxy_stream( self.hass, request, stream, 'multipart/x-mixed-replace;boundary=ffserver') - yield from stream.close() + await stream.close() @property def name(self): diff --git a/homeassistant/components/camera/canary.py b/homeassistant/components/camera/canary.py index 9031c27b1a9..b9951d8efa2 100644 --- a/homeassistant/components/camera/canary.py +++ b/homeassistant/components/camera/canary.py @@ -75,35 +75,33 @@ class CanaryCamera(Camera): """Return the camera motion detection status.""" return not self._location.is_recording - @asyncio.coroutine - def async_camera_image(self): + async def async_camera_image(self): """Return a still image response from the camera.""" self.renew_live_stream_session() from haffmpeg import ImageFrame, IMAGE_JPEG ffmpeg = ImageFrame(self._ffmpeg.binary, loop=self.hass.loop) - image = yield from asyncio.shield(ffmpeg.get_image( + image = await asyncio.shield(ffmpeg.get_image( self._live_stream_session.live_stream_url, output_format=IMAGE_JPEG, extra_cmd=self._ffmpeg_arguments), loop=self.hass.loop) return image - @asyncio.coroutine - def handle_async_mjpeg_stream(self, request): + async def handle_async_mjpeg_stream(self, request): """Generate an HTTP MJPEG stream from the camera.""" if self._live_stream_session is None: return from haffmpeg import CameraMjpeg stream = CameraMjpeg(self._ffmpeg.binary, loop=self.hass.loop) - yield from stream.open_camera( + await stream.open_camera( self._live_stream_session.live_stream_url, extra_cmd=self._ffmpeg_arguments) - yield from async_aiohttp_proxy_stream( + await async_aiohttp_proxy_stream( self.hass, request, stream, 'multipart/x-mixed-replace;boundary=ffserver') - yield from stream.close() + await stream.close() @Throttle(MIN_TIME_BETWEEN_SESSION_RENEW) def renew_live_stream_session(self): diff --git a/homeassistant/components/camera/doorbird.py b/homeassistant/components/camera/doorbird.py index 7af3e7634d0..8982e6d0847 100644 --- a/homeassistant/components/camera/doorbird.py +++ b/homeassistant/components/camera/doorbird.py @@ -27,9 +27,8 @@ _LOGGER = logging.getLogger(__name__) _TIMEOUT = 10 # seconds -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the DoorBird camera platform.""" for doorstation in hass.data[DOORBIRD_DOMAIN]: device = doorstation.device @@ -66,8 +65,7 @@ class DoorBirdCamera(Camera): """Get the name of the camera.""" return self._name - @asyncio.coroutine - def async_camera_image(self): + async def async_camera_image(self): """Pull a still image from the camera.""" now = datetime.datetime.now() @@ -77,9 +75,9 @@ class DoorBirdCamera(Camera): try: websession = async_get_clientsession(self.hass) with async_timeout.timeout(_TIMEOUT, loop=self.hass.loop): - response = yield from websession.get(self._url) + response = await websession.get(self._url) - self._last_image = yield from response.read() + self._last_image = await response.read() self._last_update = now return self._last_image except asyncio.TimeoutError: diff --git a/homeassistant/components/camera/generic.py b/homeassistant/components/camera/generic.py index b707c913435..f89e5ff29c2 100644 --- a/homeassistant/components/camera/generic.py +++ b/homeassistant/components/camera/generic.py @@ -46,9 +46,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up a generic IP Camera.""" async_add_entities([GenericCamera(hass, config)]) @@ -93,8 +92,7 @@ class GenericCamera(Camera): return run_coroutine_threadsafe( self.async_camera_image(), self.hass.loop).result() - @asyncio.coroutine - def async_camera_image(self): + async def async_camera_image(self): """Return a still image response from the camera.""" try: url = self._still_image_url.async_render() @@ -118,7 +116,7 @@ class GenericCamera(Camera): _LOGGER.error("Error getting camera image: %s", error) return self._last_image - self._last_image = yield from self.hass.async_add_job( + self._last_image = await self.hass.async_add_job( fetch) # async else: @@ -126,9 +124,9 @@ class GenericCamera(Camera): websession = async_get_clientsession( self.hass, verify_ssl=self.verify_ssl) with async_timeout.timeout(10, loop=self.hass.loop): - response = yield from websession.get( + response = await websession.get( url, auth=self._auth) - self._last_image = yield from response.read() + self._last_image = await response.read() except asyncio.TimeoutError: _LOGGER.error("Timeout getting camera image") return self._last_image diff --git a/homeassistant/components/camera/mjpeg.py b/homeassistant/components/camera/mjpeg.py index f1917aaf23e..b51ec982ccd 100644 --- a/homeassistant/components/camera/mjpeg.py +++ b/homeassistant/components/camera/mjpeg.py @@ -41,9 +41,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up a MJPEG IP Camera.""" if discovery_info: config = PLATFORM_SCHEMA(discovery_info) @@ -82,23 +81,22 @@ class MjpegCamera(Camera): self._username, password=self._password ) - @asyncio.coroutine - def async_camera_image(self): + async def async_camera_image(self): """Return a still image response from the camera.""" # DigestAuth is not supported if self._authentication == HTTP_DIGEST_AUTHENTICATION or \ self._still_image_url is None: - image = yield from self.hass.async_add_job( + image = await self.hass.async_add_job( self.camera_image) return image websession = async_get_clientsession(self.hass) try: with async_timeout.timeout(10, loop=self.hass.loop): - response = yield from websession.get( + response = await websession.get( self._still_image_url, auth=self._auth) - image = yield from response.read() + image = await response.read() return image except asyncio.TimeoutError: diff --git a/homeassistant/components/camera/ring.py b/homeassistant/components/camera/ring.py index d0cb6443fc7..ae886bd0669 100644 --- a/homeassistant/components/camera/ring.py +++ b/homeassistant/components/camera/ring.py @@ -110,8 +110,7 @@ class RingCam(Camera): 'video_url': self._video_url, } - @asyncio.coroutine - def async_camera_image(self): + async def async_camera_image(self): """Return a still image response from the camera.""" from haffmpeg import ImageFrame, IMAGE_JPEG ffmpeg = ImageFrame(self._ffmpeg.binary, loop=self.hass.loop) @@ -119,13 +118,12 @@ class RingCam(Camera): if self._video_url is None: return - image = yield from asyncio.shield(ffmpeg.get_image( + image = await asyncio.shield(ffmpeg.get_image( self._video_url, output_format=IMAGE_JPEG, extra_cmd=self._ffmpeg_arguments), loop=self.hass.loop) return image - @asyncio.coroutine - def handle_async_mjpeg_stream(self, request): + async def handle_async_mjpeg_stream(self, request): """Generate an HTTP MJPEG stream from the camera.""" from haffmpeg import CameraMjpeg @@ -133,13 +131,13 @@ class RingCam(Camera): return stream = CameraMjpeg(self._ffmpeg.binary, loop=self.hass.loop) - yield from stream.open_camera( + await stream.open_camera( self._video_url, extra_cmd=self._ffmpeg_arguments) - yield from async_aiohttp_proxy_stream( + await async_aiohttp_proxy_stream( self.hass, request, stream, 'multipart/x-mixed-replace;boundary=ffserver') - yield from stream.close() + await stream.close() @property def should_poll(self): diff --git a/homeassistant/components/camera/synology.py b/homeassistant/components/camera/synology.py index 3e587fff234..b504fe34d86 100644 --- a/homeassistant/components/camera/synology.py +++ b/homeassistant/components/camera/synology.py @@ -4,7 +4,6 @@ Support for Synology Surveillance Station Cameras. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/camera.synology/ """ -import asyncio import logging import requests @@ -38,9 +37,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up a Synology IP Camera.""" verify_ssl = config.get(CONF_VERIFY_SSL) timeout = config.get(CONF_TIMEOUT) @@ -87,15 +85,14 @@ class SynologyCamera(Camera): """Return bytes of camera image.""" return self._surveillance.get_camera_image(self._camera_id) - @asyncio.coroutine - def handle_async_mjpeg_stream(self, request): + async def handle_async_mjpeg_stream(self, request): """Return a MJPEG stream image response directly from the camera.""" streaming_url = self._camera.video_stream_url websession = async_get_clientsession(self.hass, self._verify_ssl) stream_coro = websession.get(streaming_url) - yield from async_aiohttp_proxy_web(self.hass, request, stream_coro) + await async_aiohttp_proxy_web(self.hass, request, stream_coro) @property def name(self): diff --git a/homeassistant/components/climate/generic_thermostat.py b/homeassistant/components/climate/generic_thermostat.py index 85879b8122a..258699ff90a 100644 --- a/homeassistant/components/climate/generic_thermostat.py +++ b/homeassistant/components/climate/generic_thermostat.py @@ -67,9 +67,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the generic thermostat platform.""" name = config.get(CONF_NAME) heater_entity_id = config.get(CONF_HEATER) @@ -147,12 +146,10 @@ class GenericThermostat(ClimateDevice): if sensor_state and sensor_state.state != STATE_UNKNOWN: self._async_update_temp(sensor_state) - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Run when entity about to be added.""" # Check If we have an old state - old_state = yield from async_get_last_state(self.hass, - self.entity_id) + old_state = await async_get_last_state(self.hass, self.entity_id) if old_state is not None: # If we have no initial temperature, restore if self._target_temp is None: diff --git a/homeassistant/components/climate/mqtt.py b/homeassistant/components/climate/mqtt.py index 23def7c4b87..79c49db7955 100644 --- a/homeassistant/components/climate/mqtt.py +++ b/homeassistant/components/climate/mqtt.py @@ -4,7 +4,6 @@ Support for MQTT climate devices. For more details about this platform, please refer to the documentation https://home-assistant.io/components/climate.mqtt/ """ -import asyncio import logging import voluptuous as vol @@ -258,11 +257,10 @@ class MqttClimate(MqttAvailability, MqttDiscoveryUpdate, ClimateDevice): self._max_temp = max_temp self._discovery_hash = discovery_hash - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Handle being added to home assistant.""" - yield from MqttAvailability.async_added_to_hass(self) - yield from MqttDiscoveryUpdate.async_added_to_hass(self) + await MqttAvailability.async_added_to_hass(self) + await MqttDiscoveryUpdate.async_added_to_hass(self) @callback def handle_current_temp_received(topic, payload, qos): @@ -279,7 +277,7 @@ class MqttClimate(MqttAvailability, MqttDiscoveryUpdate, ClimateDevice): _LOGGER.error("Could not parse temperature from %s", payload) if self._topic[CONF_CURRENT_TEMPERATURE_TOPIC] is not None: - yield from mqtt.async_subscribe( + await mqtt.async_subscribe( self.hass, self._topic[CONF_CURRENT_TEMPERATURE_TOPIC], handle_current_temp_received, self._qos) @@ -297,7 +295,7 @@ class MqttClimate(MqttAvailability, MqttDiscoveryUpdate, ClimateDevice): self.async_schedule_update_ha_state() if self._topic[CONF_MODE_STATE_TOPIC] is not None: - yield from mqtt.async_subscribe( + await mqtt.async_subscribe( self.hass, self._topic[CONF_MODE_STATE_TOPIC], handle_mode_received, self._qos) @@ -316,7 +314,7 @@ class MqttClimate(MqttAvailability, MqttDiscoveryUpdate, ClimateDevice): _LOGGER.error("Could not parse temperature from %s", payload) if self._topic[CONF_TEMPERATURE_STATE_TOPIC] is not None: - yield from mqtt.async_subscribe( + await mqtt.async_subscribe( self.hass, self._topic[CONF_TEMPERATURE_STATE_TOPIC], handle_temperature_received, self._qos) @@ -335,7 +333,7 @@ class MqttClimate(MqttAvailability, MqttDiscoveryUpdate, ClimateDevice): self.async_schedule_update_ha_state() if self._topic[CONF_FAN_MODE_STATE_TOPIC] is not None: - yield from mqtt.async_subscribe( + await mqtt.async_subscribe( self.hass, self._topic[CONF_FAN_MODE_STATE_TOPIC], handle_fan_mode_received, self._qos) @@ -354,7 +352,7 @@ class MqttClimate(MqttAvailability, MqttDiscoveryUpdate, ClimateDevice): self.async_schedule_update_ha_state() if self._topic[CONF_SWING_MODE_STATE_TOPIC] is not None: - yield from mqtt.async_subscribe( + await mqtt.async_subscribe( self.hass, self._topic[CONF_SWING_MODE_STATE_TOPIC], handle_swing_mode_received, self._qos) @@ -380,7 +378,7 @@ class MqttClimate(MqttAvailability, MqttDiscoveryUpdate, ClimateDevice): self.async_schedule_update_ha_state() if self._topic[CONF_AWAY_MODE_STATE_TOPIC] is not None: - yield from mqtt.async_subscribe( + await mqtt.async_subscribe( self.hass, self._topic[CONF_AWAY_MODE_STATE_TOPIC], handle_away_mode_received, self._qos) @@ -405,7 +403,7 @@ class MqttClimate(MqttAvailability, MqttDiscoveryUpdate, ClimateDevice): self.async_schedule_update_ha_state() if self._topic[CONF_AUX_STATE_TOPIC] is not None: - yield from mqtt.async_subscribe( + await mqtt.async_subscribe( self.hass, self._topic[CONF_AUX_STATE_TOPIC], handle_aux_mode_received, self._qos) @@ -420,7 +418,7 @@ class MqttClimate(MqttAvailability, MqttDiscoveryUpdate, ClimateDevice): self.async_schedule_update_ha_state() if self._topic[CONF_HOLD_STATE_TOPIC] is not None: - yield from mqtt.async_subscribe( + await mqtt.async_subscribe( self.hass, self._topic[CONF_HOLD_STATE_TOPIC], handle_hold_mode_received, self._qos) @@ -489,12 +487,11 @@ class MqttClimate(MqttAvailability, MqttDiscoveryUpdate, ClimateDevice): """Return the list of available fan modes.""" return self._fan_list - @asyncio.coroutine - def async_set_temperature(self, **kwargs): + async def async_set_temperature(self, **kwargs): """Set new target temperatures.""" if kwargs.get(ATTR_OPERATION_MODE) is not None: operation_mode = kwargs.get(ATTR_OPERATION_MODE) - yield from self.async_set_operation_mode(operation_mode) + await self.async_set_operation_mode(operation_mode) if kwargs.get(ATTR_TEMPERATURE) is not None: if self._topic[CONF_TEMPERATURE_STATE_TOPIC] is None: @@ -508,8 +505,7 @@ class MqttClimate(MqttAvailability, MqttDiscoveryUpdate, ClimateDevice): self.async_schedule_update_ha_state() - @asyncio.coroutine - def async_set_swing_mode(self, swing_mode): + async def async_set_swing_mode(self, swing_mode): """Set new swing mode.""" if self._send_if_off or self._current_operation != STATE_OFF: mqtt.async_publish( @@ -520,8 +516,7 @@ class MqttClimate(MqttAvailability, MqttDiscoveryUpdate, ClimateDevice): self._current_swing_mode = swing_mode self.async_schedule_update_ha_state() - @asyncio.coroutine - def async_set_fan_mode(self, fan_mode): + async def async_set_fan_mode(self, fan_mode): """Set new target temperature.""" if self._send_if_off or self._current_operation != STATE_OFF: mqtt.async_publish( @@ -532,8 +527,7 @@ class MqttClimate(MqttAvailability, MqttDiscoveryUpdate, ClimateDevice): self._current_fan_mode = fan_mode self.async_schedule_update_ha_state() - @asyncio.coroutine - def async_set_operation_mode(self, operation_mode) -> None: + async def async_set_operation_mode(self, operation_mode) -> None: """Set new operation mode.""" if self._topic[CONF_POWER_COMMAND_TOPIC] is not None: if (self._current_operation == STATE_OFF and @@ -566,8 +560,7 @@ class MqttClimate(MqttAvailability, MqttDiscoveryUpdate, ClimateDevice): """List of available swing modes.""" return self._swing_list - @asyncio.coroutine - def async_turn_away_mode_on(self): + async def async_turn_away_mode_on(self): """Turn away mode on.""" if self._topic[CONF_AWAY_MODE_COMMAND_TOPIC] is not None: mqtt.async_publish(self.hass, @@ -578,8 +571,7 @@ class MqttClimate(MqttAvailability, MqttDiscoveryUpdate, ClimateDevice): self._away = True self.async_schedule_update_ha_state() - @asyncio.coroutine - def async_turn_away_mode_off(self): + async def async_turn_away_mode_off(self): """Turn away mode off.""" if self._topic[CONF_AWAY_MODE_COMMAND_TOPIC] is not None: mqtt.async_publish(self.hass, @@ -590,8 +582,7 @@ class MqttClimate(MqttAvailability, MqttDiscoveryUpdate, ClimateDevice): self._away = False self.async_schedule_update_ha_state() - @asyncio.coroutine - def async_set_hold_mode(self, hold_mode): + async def async_set_hold_mode(self, hold_mode): """Update hold mode on.""" if self._topic[CONF_HOLD_COMMAND_TOPIC] is not None: mqtt.async_publish(self.hass, @@ -602,8 +593,7 @@ class MqttClimate(MqttAvailability, MqttDiscoveryUpdate, ClimateDevice): self._hold = hold_mode self.async_schedule_update_ha_state() - @asyncio.coroutine - def async_turn_aux_heat_on(self): + async def async_turn_aux_heat_on(self): """Turn auxiliary heater on.""" if self._topic[CONF_AUX_COMMAND_TOPIC] is not None: mqtt.async_publish(self.hass, self._topic[CONF_AUX_COMMAND_TOPIC], @@ -613,8 +603,7 @@ class MqttClimate(MqttAvailability, MqttDiscoveryUpdate, ClimateDevice): self._aux = True self.async_schedule_update_ha_state() - @asyncio.coroutine - def async_turn_aux_heat_off(self): + async def async_turn_aux_heat_off(self): """Turn auxiliary heater off.""" if self._topic[CONF_AUX_COMMAND_TOPIC] is not None: mqtt.async_publish(self.hass, self._topic[CONF_AUX_COMMAND_TOPIC], diff --git a/homeassistant/components/climate/radiotherm.py b/homeassistant/components/climate/radiotherm.py index 14cd2a0f02e..f914b9b4762 100644 --- a/homeassistant/components/climate/radiotherm.py +++ b/homeassistant/components/climate/radiotherm.py @@ -4,7 +4,6 @@ Support for Radio Thermostat wifi-enabled home thermostats. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/climate.radiotherm/ """ -import asyncio import datetime import logging @@ -145,8 +144,7 @@ class RadioThermostat(ClimateDevice): """Return the list of supported features.""" return SUPPORT_FLAGS - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Register callbacks.""" # Set the time on the device. This shouldn't be in the # constructor because it's a network call. We can't put it in diff --git a/homeassistant/components/climate/sensibo.py b/homeassistant/components/climate/sensibo.py index ef33ee8495e..8532c611d25 100644 --- a/homeassistant/components/climate/sensibo.py +++ b/homeassistant/components/climate/sensibo.py @@ -58,9 +58,8 @@ FIELD_TO_FLAG = { } -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up Sensibo devices.""" import pysensibo @@ -70,7 +69,7 @@ def async_setup_platform(hass, config, async_add_entities, devices = [] try: for dev in ( - yield from client.async_get_devices(_INITIAL_FETCH_FIELDS)): + await client.async_get_devices(_INITIAL_FETCH_FIELDS)): if config[CONF_ID] == ALL or dev['id'] in config[CONF_ID]: devices.append(SensiboClimate( client, dev, hass.config.units.temperature_unit)) @@ -82,8 +81,7 @@ def async_setup_platform(hass, config, async_add_entities, if devices: async_add_entities(devices) - @asyncio.coroutine - def async_assume_state(service): + async def async_assume_state(service): """Set state according to external service call..""" entity_ids = service.data.get(ATTR_ENTITY_ID) if entity_ids: @@ -94,12 +92,12 @@ def async_setup_platform(hass, config, async_add_entities, update_tasks = [] for climate in target_climate: - yield from climate.async_assume_state( + await climate.async_assume_state( service.data.get(ATTR_STATE)) update_tasks.append(climate.async_update_ha_state(True)) if update_tasks: - yield from asyncio.wait(update_tasks, loop=hass.loop) + await asyncio.wait(update_tasks, loop=hass.loop) hass.services.async_register( DOMAIN, SERVICE_ASSUME_STATE, async_assume_state, schema=ASSUME_STATE_SCHEMA) @@ -262,8 +260,7 @@ class SensiboClimate(ClimateDevice): """Return unique ID based on Sensibo ID.""" return self._id - @asyncio.coroutine - def async_set_temperature(self, **kwargs): + async def async_set_temperature(self, **kwargs): """Set new target temperature.""" temperature = kwargs.get(ATTR_TEMPERATURE) if temperature is None: @@ -283,52 +280,46 @@ class SensiboClimate(ClimateDevice): return with async_timeout.timeout(TIMEOUT): - yield from self._client.async_set_ac_state_property( + await self._client.async_set_ac_state_property( self._id, 'targetTemperature', temperature, self._ac_states) - @asyncio.coroutine - def async_set_fan_mode(self, fan_mode): + async def async_set_fan_mode(self, fan_mode): """Set new target fan mode.""" with async_timeout.timeout(TIMEOUT): - yield from self._client.async_set_ac_state_property( + await self._client.async_set_ac_state_property( self._id, 'fanLevel', fan_mode, self._ac_states) - @asyncio.coroutine - def async_set_operation_mode(self, operation_mode): + async def async_set_operation_mode(self, operation_mode): """Set new target operation mode.""" with async_timeout.timeout(TIMEOUT): - yield from self._client.async_set_ac_state_property( + await self._client.async_set_ac_state_property( self._id, 'mode', operation_mode, self._ac_states) - @asyncio.coroutine - def async_set_swing_mode(self, swing_mode): + async def async_set_swing_mode(self, swing_mode): """Set new target swing operation.""" with async_timeout.timeout(TIMEOUT): - yield from self._client.async_set_ac_state_property( + await self._client.async_set_ac_state_property( self._id, 'swing', swing_mode, self._ac_states) - @asyncio.coroutine - def async_turn_on(self): + async def async_turn_on(self): """Turn Sensibo unit on.""" with async_timeout.timeout(TIMEOUT): - yield from self._client.async_set_ac_state_property( + await self._client.async_set_ac_state_property( self._id, 'on', True, self._ac_states) - @asyncio.coroutine - def async_turn_off(self): + async def async_turn_off(self): """Turn Sensibo unit on.""" with async_timeout.timeout(TIMEOUT): - yield from self._client.async_set_ac_state_property( + await self._client.async_set_ac_state_property( self._id, 'on', False, self._ac_states) - @asyncio.coroutine - def async_assume_state(self, state): + async def async_assume_state(self, state): """Set external state.""" change_needed = (state != STATE_OFF and not self.is_on) \ or (state == STATE_OFF and self.is_on) if change_needed: with async_timeout.timeout(TIMEOUT): - yield from self._client.async_set_ac_state_property( + await self._client.async_set_ac_state_property( self._id, 'on', state != STATE_OFF, # value @@ -341,12 +332,11 @@ class SensiboClimate(ClimateDevice): else: self._external_state = state - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Retrieve latest state.""" try: with async_timeout.timeout(TIMEOUT): - data = yield from self._client.async_get_device( + data = await self._client.async_get_device( self._id, _FETCH_FIELDS) self._do_update(data) except aiohttp.client_exceptions.ClientError: diff --git a/homeassistant/components/climate/wink.py b/homeassistant/components/climate/wink.py index 3013a155380..cb6204d3ba3 100644 --- a/homeassistant/components/climate/wink.py +++ b/homeassistant/components/climate/wink.py @@ -4,7 +4,6 @@ Support for Wink thermostats, Air Conditioners, and Water Heaters. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/climate.wink/ """ -import asyncio import logging from homeassistant.components.climate import ( @@ -92,8 +91,7 @@ class WinkThermostat(WinkDevice, ClimateDevice): """Return the list of supported features.""" return SUPPORT_FLAGS_THERMOSTAT - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Call when entity is added to hass.""" self.hass.data[DOMAIN]['entities']['climate'].append(self) diff --git a/homeassistant/components/config/automation.py b/homeassistant/components/config/automation.py index 2d13ac07025..7836cba6cf9 100644 --- a/homeassistant/components/config/automation.py +++ b/homeassistant/components/config/automation.py @@ -1,5 +1,4 @@ """Provide configuration end points for Automations.""" -import asyncio from collections import OrderedDict import uuid @@ -12,8 +11,7 @@ import homeassistant.helpers.config_validation as cv CONFIG_PATH = 'automations.yaml' -@asyncio.coroutine -def async_setup(hass): +async def async_setup(hass): """Set up the Automation config API.""" async def hook(hass): """post_write_hook for Config View that reloads automations.""" diff --git a/homeassistant/components/config/config_entries.py b/homeassistant/components/config/config_entries.py index 73b2767be4b..644990d7185 100644 --- a/homeassistant/components/config/config_entries.py +++ b/homeassistant/components/config/config_entries.py @@ -1,5 +1,4 @@ """Http views to control the config manager.""" -import asyncio from homeassistant import config_entries, data_entry_flow from homeassistant.components.http import HomeAssistantView @@ -7,8 +6,7 @@ from homeassistant.helpers.data_entry_flow import ( FlowManagerIndexView, FlowManagerResourceView) -@asyncio.coroutine -def async_setup(hass): +async def async_setup(hass): """Enable the Home Assistant views.""" hass.http.register_view(ConfigManagerEntryIndexView) hass.http.register_view(ConfigManagerEntryResourceView) @@ -44,8 +42,7 @@ class ConfigManagerEntryIndexView(HomeAssistantView): url = '/api/config/config_entries/entry' name = 'api:config:config_entries:entry' - @asyncio.coroutine - def get(self, request): + async def get(self, request): """List flows in progress.""" hass = request.app['hass'] return self.json([{ @@ -64,13 +61,12 @@ class ConfigManagerEntryResourceView(HomeAssistantView): url = '/api/config/config_entries/entry/{entry_id}' name = 'api:config:config_entries:entry:resource' - @asyncio.coroutine - def delete(self, request, entry_id): + async def delete(self, request, entry_id): """Delete a config entry.""" hass = request.app['hass'] try: - result = yield from hass.config_entries.async_remove(entry_id) + result = await hass.config_entries.async_remove(entry_id) except config_entries.UnknownEntry: return self.json_message('Invalid entry specified', 404) @@ -83,8 +79,7 @@ class ConfigManagerFlowIndexView(FlowManagerIndexView): url = '/api/config/config_entries/flow' name = 'api:config:config_entries:flow' - @asyncio.coroutine - def get(self, request): + async def get(self, request): """List flows that are in progress but not started by a user. Example of a non-user initiated flow is a discovered Hue hub that @@ -110,7 +105,6 @@ class ConfigManagerAvailableFlowView(HomeAssistantView): url = '/api/config/config_entries/flow_handlers' name = 'api:config:config_entries:flow_handlers' - @asyncio.coroutine - def get(self, request): + async def get(self, request): """List available flow handlers.""" return self.json(config_entries.FLOWS) diff --git a/homeassistant/components/config/core.py b/homeassistant/components/config/core.py index 4ff530ad2bc..ce7675c41f4 100644 --- a/homeassistant/components/config/core.py +++ b/homeassistant/components/config/core.py @@ -1,12 +1,10 @@ """Component to interact with Hassbian tools.""" -import asyncio from homeassistant.components.http import HomeAssistantView from homeassistant.config import async_check_ha_config_file -@asyncio.coroutine -def async_setup(hass): +async def async_setup(hass): """Set up the Hassbian config.""" hass.http.register_view(CheckConfigView) return True @@ -18,10 +16,9 @@ class CheckConfigView(HomeAssistantView): url = '/api/config/core/check_config' name = 'api:config:core:check_config' - @asyncio.coroutine - def post(self, request): + async def post(self, request): """Validate configuration and return results.""" - errors = yield from async_check_ha_config_file(request.app['hass']) + errors = await async_check_ha_config_file(request.app['hass']) state = 'invalid' if errors else 'valid' diff --git a/homeassistant/components/config/customize.py b/homeassistant/components/config/customize.py index d25992ecc90..eee63005944 100644 --- a/homeassistant/components/config/customize.py +++ b/homeassistant/components/config/customize.py @@ -1,5 +1,4 @@ """Provide configuration end points for Customize.""" -import asyncio from homeassistant.components.config import EditKeyBasedConfigView from homeassistant.components import async_reload_core_config @@ -10,8 +9,7 @@ import homeassistant.helpers.config_validation as cv CONFIG_PATH = 'customize.yaml' -@asyncio.coroutine -def async_setup(hass): +async def async_setup(hass): """Set up the Customize config API.""" hass.http.register_view(CustomizeConfigView( 'customize', 'config', CONFIG_PATH, cv.entity_id, dict, diff --git a/homeassistant/components/config/group.py b/homeassistant/components/config/group.py index 8b327faa95f..f9b9a2c4918 100644 --- a/homeassistant/components/config/group.py +++ b/homeassistant/components/config/group.py @@ -1,5 +1,4 @@ """Provide configuration end points for Groups.""" -import asyncio from homeassistant.const import SERVICE_RELOAD from homeassistant.components.config import EditKeyBasedConfigView from homeassistant.components.group import DOMAIN, GROUP_SCHEMA @@ -9,13 +8,11 @@ import homeassistant.helpers.config_validation as cv CONFIG_PATH = 'groups.yaml' -@asyncio.coroutine -def async_setup(hass): +async def async_setup(hass): """Set up the Group config API.""" - @asyncio.coroutine - def hook(hass): + async def hook(hass): """post_write_hook for Config View that reloads groups.""" - yield from hass.services.async_call(DOMAIN, SERVICE_RELOAD) + await hass.services.async_call(DOMAIN, SERVICE_RELOAD) hass.http.register_view(EditKeyBasedConfigView( 'group', 'config', CONFIG_PATH, cv.slug, GROUP_SCHEMA, diff --git a/homeassistant/components/config/hassbian.py b/homeassistant/components/config/hassbian.py index 8de5f62d915..c475dc317f7 100644 --- a/homeassistant/components/config/hassbian.py +++ b/homeassistant/components/config/hassbian.py @@ -1,5 +1,4 @@ """Component to interact with Hassbian tools.""" -import asyncio import json import os @@ -30,8 +29,7 @@ _TEST_OUTPUT = """ """ # noqa -@asyncio.coroutine -def async_setup(hass): +async def async_setup(hass): """Set up the Hassbian config.""" # Test if is Hassbian test_mode = 'FORCE_HASSBIAN' in os.environ @@ -46,8 +44,7 @@ def async_setup(hass): return True -@asyncio.coroutine -def hassbian_status(hass, test_mode=False): +async def hassbian_status(hass, test_mode=False): """Query for the Hassbian status.""" # Fetch real output when not in test mode if test_mode: @@ -66,10 +63,9 @@ class HassbianSuitesView(HomeAssistantView): """Initialize suites view.""" self._test_mode = test_mode - @asyncio.coroutine - def get(self, request): + async def get(self, request): """Request suite status.""" - inp = yield from hassbian_status(request.app['hass'], self._test_mode) + inp = await hassbian_status(request.app['hass'], self._test_mode) return self.json(inp['suites']) @@ -84,8 +80,7 @@ class HassbianSuiteInstallView(HomeAssistantView): """Initialize suite view.""" self._test_mode = test_mode - @asyncio.coroutine - def post(self, request, suite): + async def post(self, request, suite): """Request suite status.""" # do real install if not in test mode return self.json({"status": "ok"}) diff --git a/homeassistant/components/config/script.py b/homeassistant/components/config/script.py index 6f2eaa3ff67..3adc6f14233 100644 --- a/homeassistant/components/config/script.py +++ b/homeassistant/components/config/script.py @@ -1,5 +1,4 @@ """Provide configuration end points for scripts.""" -import asyncio from homeassistant.components.config import EditKeyBasedConfigView from homeassistant.components.script import DOMAIN, SCRIPT_ENTRY_SCHEMA @@ -10,8 +9,7 @@ import homeassistant.helpers.config_validation as cv CONFIG_PATH = 'scripts.yaml' -@asyncio.coroutine -def async_setup(hass): +async def async_setup(hass): """Set up the script config API.""" async def hook(hass): """post_write_hook for Config View that reloads scripts.""" diff --git a/homeassistant/components/config/zwave.py b/homeassistant/components/config/zwave.py index fcdab835052..57123ee12de 100644 --- a/homeassistant/components/config/zwave.py +++ b/homeassistant/components/config/zwave.py @@ -1,5 +1,4 @@ """Provide configuration end points for Z-Wave.""" -import asyncio import logging from collections import deque @@ -16,8 +15,7 @@ CONFIG_PATH = 'zwave_device_config.yaml' OZW_LOG_FILENAME = 'OZW_Log.txt' -@asyncio.coroutine -def async_setup(hass): +async def async_setup(hass): """Set up the Z-Wave config API.""" hass.http.register_view(EditKeyBasedConfigView( 'zwave', 'device_config', CONFIG_PATH, cv.entity_id, @@ -41,8 +39,7 @@ class ZWaveLogView(HomeAssistantView): name = "api:zwave:ozwlog" # pylint: disable=no-self-use - @asyncio.coroutine - def get(self, request): + async def get(self, request): """Retrieve the lines from ZWave log.""" try: lines = int(request.query.get('lines', 0)) @@ -50,7 +47,7 @@ class ZWaveLogView(HomeAssistantView): return Response(text='Invalid datetime', status=400) hass = request.app['hass'] - response = yield from hass.async_add_job(self._get_log, hass, lines) + response = await hass.async_add_job(self._get_log, hass, lines) return Response(text='\n'.join(response)) diff --git a/homeassistant/components/configurator.py b/homeassistant/components/configurator.py index 56fb7b4247b..74d8339b1fa 100644 --- a/homeassistant/components/configurator.py +++ b/homeassistant/components/configurator.py @@ -6,7 +6,6 @@ This will return a request id that has to be used for future calls. A callback has to be provided to `request_config` which will be called when the user has submitted configuration information. """ -import asyncio import functools as ft import logging @@ -122,8 +121,7 @@ def request_done(hass, request_id): ).result() -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Set up the configurator component.""" return True @@ -207,8 +205,7 @@ class Configurator: self.hass.bus.async_listen_once(EVENT_TIME_CHANGED, deferred_remove) - @asyncio.coroutine - def async_handle_service_call(self, call): + async def async_handle_service_call(self, call): """Handle a configure service call.""" request_id = call.data.get(ATTR_CONFIGURE_ID) @@ -220,8 +217,8 @@ class Configurator: # field validation goes here? if callback: - yield from self.hass.async_add_job(callback, - call.data.get(ATTR_FIELDS, {})) + await self.hass.async_add_job(callback, + call.data.get(ATTR_FIELDS, {})) def _generate_unique_id(self): """Generate a unique configurator ID.""" From ea7b1e4573c4e2c2c8e6c06dc0fc68d14ceb495a Mon Sep 17 00:00:00 2001 From: Charles Garwood Date: Mon, 1 Oct 2018 02:50:32 -0400 Subject: [PATCH 118/247] Update Z-Wave service descriptions to point to proper log file (#17024) --- homeassistant/components/zwave/services.yaml | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/zwave/services.yaml b/homeassistant/components/zwave/services.yaml index 583d76a1955..7c926a5a879 100644 --- a/homeassistant/components/zwave/services.yaml +++ b/homeassistant/components/zwave/services.yaml @@ -18,40 +18,40 @@ change_association: description: (Optional) Instance of multichannel association. Defaults to 0. add_node: - description: Add a new (unsecure) node to the Z-Wave network. Refer to OZW.log for progress. + description: Add a new (unsecure) node to the Z-Wave network. Refer to OZW_Log.txt for progress. add_node_secure: - description: Add a new node to the Z-Wave network with secure communications. Secure network key must be set, this process will fallback to add_node (unsecure) for unsupported devices. Note that unsecure devices can't directly talk to secure devices. Refer to OZW.log for progress. + description: Add a new node to the Z-Wave network with secure communications. Secure network key must be set, this process will fallback to add_node (unsecure) for unsupported devices. Note that unsecure devices can't directly talk to secure devices. Refer to OZW_Log.txt for progress. cancel_command: description: Cancel a running Z-Wave controller command. Use this to exit add_node, if you weren't going to use it but activated it. heal_network: - description: Start a Z-Wave network heal. This might take a while and will slow down the Z-Wave network greatly while it is being processed. Refer to OZW.log for progress. + description: Start a Z-Wave network heal. This might take a while and will slow down the Z-Wave network greatly while it is being processed. Refer to OZW_Log.txt for progress. fields: return_routes: description: Whether or not to update the return routes from the nodes to the controller. Defaults to False. example: True heal_node: - description: Start a Z-Wave node heal. Refer to OZW.log for progress. + description: Start a Z-Wave node heal. Refer to OZW_Log.txt for progress. fields: return_routes: description: Whether or not to update the return routes from the node to the controller. Defaults to False. example: True remove_node: - description: Remove a node from the Z-Wave network. Refer to OZW.log for progress. + description: Remove a node from the Z-Wave network. Refer to OZW_Log.txt for progress. remove_failed_node: - description: This command will remove a failed node from the network. The node should be on the controllers failed nodes list, otherwise this command will fail. Refer to OZW.log for progress. + description: This command will remove a failed node from the network. The node should be on the controller's failed nodes list, otherwise this command will fail. Refer to OZW_Log.txt for progress. fields: node_id: description: Node id of the device to remove (integer). example: 10 replace_failed_node: - description: Replace a failed node with another. If the node is not in the controller's failed nodes list, or the node responds, this command will fail. Refer to OZW.log for progress. + description: Replace a failed node with another. If the node is not in the controller's failed nodes list, or the node responds, this command will fail. Refer to OZW_Log.txt for progress. fields: node_id: description: Node id of the device to replace (integer). @@ -147,10 +147,10 @@ stop_network: description: Stop the Z-Wave network, all updates into HASS will stop. soft_reset: - description: This will reset the controller without removing its data. Use carefully because not all controllers support this. Refer to controllers manual. + description: This will reset the controller without removing its data. Use carefully because not all controllers support this. Refer to your controller's manual. test_network: - description: This will send test to nodes in the Z-Wave network. This will greatly slow down the Z-Wave network while it is being processed. Refer to OZW.log for progress. + description: This will send test to nodes in the Z-Wave network. This will greatly slow down the Z-Wave network while it is being processed. Refer to OZW_Log.txt for progress. test_node: description: This will send test messages to a node in the Z-Wave network. This could bring back dead nodes. From 9aaf11de8c159d4b9a719709bfa373cfd341e48a Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Mon, 1 Oct 2018 08:52:42 +0200 Subject: [PATCH 119/247] Async syntax 8/8 (#17022) * Async syntax 8 * Pylint fixes --- homeassistant/components/__init__.py | 25 +++----- homeassistant/components/abode.py | 7 +-- homeassistant/components/alexa/__init__.py | 4 +- homeassistant/components/alexa/intent.py | 21 +++---- homeassistant/components/alexa/smart_home.py | 8 +-- homeassistant/components/android_ip_webcam.py | 18 +++--- homeassistant/components/apple_tv.py | 34 +++++------ homeassistant/components/cloud/__init__.py | 19 +++--- homeassistant/components/demo.py | 7 +-- .../components/device_sun_light_trigger.py | 4 +- homeassistant/components/doorbird.py | 4 +- homeassistant/components/duckdns.py | 24 +++----- .../components/emulated_hue/hue_api.py | 11 ++-- homeassistant/components/envisalink.py | 5 +- homeassistant/components/ffmpeg.py | 40 +++++------- homeassistant/components/foursquare.py | 6 +- homeassistant/components/freedns.py | 17 +++--- homeassistant/components/google_domains.py | 18 +++--- .../components/homematic/__init__.py | 6 +- homeassistant/components/hydrawise.py | 4 +- homeassistant/components/ihc/ihcdevice.py | 4 +- homeassistant/components/insteon/__init__.py | 11 ++-- homeassistant/components/insteon_plm.py | 4 +- homeassistant/components/intent_script.py | 9 +-- homeassistant/components/introduction.py | 4 +- homeassistant/components/ios/__init__.py | 6 +- homeassistant/components/isy994.py | 4 +- homeassistant/components/lutron.py | 4 +- homeassistant/components/lutron_caseta.py | 9 +-- homeassistant/components/microsoft_face.py | 61 ++++++++----------- homeassistant/components/mqtt_statestream.py | 4 +- homeassistant/components/namecheapdns.py | 18 +++--- homeassistant/components/no_ip.py | 17 +++--- .../persistent_notification/__init__.py | 4 +- homeassistant/components/plant.py | 12 ++-- homeassistant/components/prometheus.py | 4 +- homeassistant/components/raincloud.py | 4 +- homeassistant/components/rest_command.py | 8 +-- homeassistant/components/rflink.py | 33 ++++------ homeassistant/components/rfxtrx.py | 4 +- homeassistant/components/rss_feed_template.py | 4 +- homeassistant/components/satel_integra.py | 10 ++- homeassistant/components/shell_command.py | 10 ++- homeassistant/components/sun.py | 4 +- .../components/system_log/__init__.py | 13 ++-- homeassistant/components/tellstick.py | 4 +- homeassistant/components/thethingsnetwork.py | 4 +- homeassistant/components/upcloud.py | 4 +- homeassistant/components/wake_on_lan.py | 11 ++-- homeassistant/components/weather/__init__.py | 6 +- .../components/weather/buienradar.py | 8 +-- homeassistant/components/wink/__init__.py | 7 +-- homeassistant/components/xiaomi_aqara.py | 7 +-- homeassistant/components/zigbee.py | 7 +-- 54 files changed, 223 insertions(+), 382 deletions(-) diff --git a/homeassistant/components/__init__.py b/homeassistant/components/__init__.py index bf1577cbf01..e8994592d40 100644 --- a/homeassistant/components/__init__.py +++ b/homeassistant/components/__init__.py @@ -103,17 +103,14 @@ def reload_core_config(hass): hass.services.call(ha.DOMAIN, SERVICE_RELOAD_CORE_CONFIG) -@asyncio.coroutine -def async_reload_core_config(hass): +async def async_reload_core_config(hass): """Reload the core config.""" - yield from hass.services.async_call(ha.DOMAIN, SERVICE_RELOAD_CORE_CONFIG) + await hass.services.async_call(ha.DOMAIN, SERVICE_RELOAD_CORE_CONFIG) -@asyncio.coroutine -def async_setup(hass: ha.HomeAssistant, config: dict) -> Awaitable[bool]: +async def async_setup(hass: ha.HomeAssistant, config: dict) -> Awaitable[bool]: """Set up general services related to Home Assistant.""" - @asyncio.coroutine - def async_handle_turn_service(service): + async def async_handle_turn_service(service): """Handle calls to homeassistant.turn_on/off.""" entity_ids = extract_entity_ids(hass, service) @@ -148,7 +145,7 @@ def async_setup(hass: ha.HomeAssistant, config: dict) -> Awaitable[bool]: tasks.append(hass.services.async_call( domain, service.service, data, blocking)) - yield from asyncio.wait(tasks, loop=hass.loop) + await asyncio.wait(tasks, loop=hass.loop) hass.services.async_register( ha.DOMAIN, SERVICE_TURN_OFF, async_handle_turn_service) @@ -164,15 +161,14 @@ def async_setup(hass: ha.HomeAssistant, config: dict) -> Awaitable[bool]: hass.helpers.intent.async_register(intent.ServiceIntentHandler( intent.INTENT_TOGGLE, ha.DOMAIN, SERVICE_TOGGLE, "Toggled {}")) - @asyncio.coroutine - def async_handle_core_service(call): + async def async_handle_core_service(call): """Service handler for handling core services.""" if call.service == SERVICE_HOMEASSISTANT_STOP: hass.async_create_task(hass.async_stop()) return try: - errors = yield from conf_util.async_check_ha_config_file(hass) + errors = await conf_util.async_check_ha_config_file(hass) except HomeAssistantError: return @@ -193,16 +189,15 @@ def async_setup(hass: ha.HomeAssistant, config: dict) -> Awaitable[bool]: hass.services.async_register( ha.DOMAIN, SERVICE_CHECK_CONFIG, async_handle_core_service) - @asyncio.coroutine - def async_handle_reload_config(call): + async def async_handle_reload_config(call): """Service handler for reloading core config.""" try: - conf = yield from conf_util.async_hass_config_yaml(hass) + conf = await conf_util.async_hass_config_yaml(hass) except HomeAssistantError as err: _LOGGER.error(err) return - yield from conf_util.async_process_ha_core_config( + await conf_util.async_process_ha_core_config( hass, conf.get(ha.DOMAIN) or {}) hass.services.async_register( diff --git a/homeassistant/components/abode.py b/homeassistant/components/abode.py index bafbc0781ca..64bedb4ac7c 100644 --- a/homeassistant/components/abode.py +++ b/homeassistant/components/abode.py @@ -4,7 +4,6 @@ This component provides basic support for Abode Home Security system. For more details about this component, please refer to the documentation at https://home-assistant.io/components/abode/ """ -import asyncio import logging from functools import partial from requests.exceptions import HTTPError, ConnectTimeout @@ -261,8 +260,7 @@ class AbodeDevice(Entity): self._data = data self._device = device - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Subscribe Abode events.""" self.hass.async_add_job( self._data.abode.events.add_device_callback, @@ -308,8 +306,7 @@ class AbodeAutomation(Entity): self._automation = automation self._event = event - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Subscribe Abode events.""" if self._event: self.hass.async_add_job( diff --git a/homeassistant/components/alexa/__init__.py b/homeassistant/components/alexa/__init__.py index d120270650f..337d8993b28 100644 --- a/homeassistant/components/alexa/__init__.py +++ b/homeassistant/components/alexa/__init__.py @@ -4,7 +4,6 @@ Support for Alexa skill service end point. For more details about this component, please refer to the documentation at https://home-assistant.io/components/alexa/ """ -import asyncio import logging import voluptuous as vol @@ -53,8 +52,7 @@ CONFIG_SCHEMA = vol.Schema({ }, extra=vol.ALLOW_EXTRA) -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Activate Alexa component.""" config = config.get(DOMAIN, {}) flash_briefings_config = config.get(CONF_FLASH_BRIEFINGS) diff --git a/homeassistant/components/alexa/intent.py b/homeassistant/components/alexa/intent.py index 8d4520d74e8..85cb4f105cd 100644 --- a/homeassistant/components/alexa/intent.py +++ b/homeassistant/components/alexa/intent.py @@ -4,7 +4,6 @@ Support for Alexa skill service end point. For more details about this component, please refer to the documentation at https://home-assistant.io/components/alexa/ """ -import asyncio import enum import logging @@ -59,16 +58,15 @@ class AlexaIntentsView(http.HomeAssistantView): url = INTENTS_API_ENDPOINT name = 'api:alexa' - @asyncio.coroutine - def post(self, request): + async def post(self, request): """Handle Alexa.""" hass = request.app['hass'] - message = yield from request.json() + message = await request.json() _LOGGER.debug("Received Alexa request: %s", message) try: - response = yield from async_handle_message(hass, message) + response = await async_handle_message(hass, message) return b'' if response is None else self.json(response) except UnknownRequest as err: _LOGGER.warning(str(err)) @@ -101,8 +99,7 @@ def intent_error_response(hass, message, error): return alexa_response.as_dict() -@asyncio.coroutine -def async_handle_message(hass, message): +async def async_handle_message(hass, message): """Handle an Alexa intent. Raises: @@ -120,20 +117,18 @@ def async_handle_message(hass, message): if not handler: raise UnknownRequest('Received unknown request {}'.format(req_type)) - return (yield from handler(hass, message)) + return await handler(hass, message) @HANDLERS.register('SessionEndedRequest') -@asyncio.coroutine -def async_handle_session_end(hass, message): +async def async_handle_session_end(hass, message): """Handle a session end request.""" return None @HANDLERS.register('IntentRequest') @HANDLERS.register('LaunchRequest') -@asyncio.coroutine -def async_handle_intent(hass, message): +async def async_handle_intent(hass, message): """Handle an intent request. Raises: @@ -153,7 +148,7 @@ def async_handle_intent(hass, message): else: intent_name = alexa_intent_info['name'] - intent_response = yield from intent.async_handle( + intent_response = await intent.async_handle( hass, DOMAIN, intent_name, {key: {'value': value} for key, value in alexa_response.variables.items()}) diff --git a/homeassistant/components/alexa/smart_home.py b/homeassistant/components/alexa/smart_home.py index 176c286ebc3..f88d81ab851 100644 --- a/homeassistant/components/alexa/smart_home.py +++ b/homeassistant/components/alexa/smart_home.py @@ -1,5 +1,4 @@ """Support for alexa Smart Home Skill API.""" -import asyncio import logging import math from datetime import datetime @@ -695,8 +694,7 @@ class SmartHomeView(http.HomeAssistantView): """Initialize.""" self.smart_home_config = smart_home_config - @asyncio.coroutine - def post(self, request): + async def post(self, request): """Handle Alexa Smart Home requests. The Smart Home API requires the endpoint to be implemented in AWS @@ -704,11 +702,11 @@ class SmartHomeView(http.HomeAssistantView): the response. """ hass = request.app['hass'] - message = yield from request.json() + message = await request.json() _LOGGER.debug("Received Alexa Smart Home request: %s", message) - response = yield from async_handle_message( + response = await async_handle_message( hass, self.smart_home_config, message) _LOGGER.debug("Sending Alexa Smart Home response: %s", response) return b'' if response is None else self.json(response) diff --git a/homeassistant/components/android_ip_webcam.py b/homeassistant/components/android_ip_webcam.py index 5da117e74c3..b8a2d461489 100644 --- a/homeassistant/components/android_ip_webcam.py +++ b/homeassistant/components/android_ip_webcam.py @@ -149,16 +149,14 @@ CONFIG_SCHEMA = vol.Schema({ }, extra=vol.ALLOW_EXTRA) -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Set up the IP Webcam component.""" from pydroid_ipcam import PyDroidIPCam webcams = hass.data[DATA_IP_WEBCAM] = {} websession = async_get_clientsession(hass) - @asyncio.coroutine - def async_setup_ipcamera(cam_config): + async def async_setup_ipcamera(cam_config): """Set up an IP camera.""" host = cam_config[CONF_HOST] username = cam_config.get(CONF_USERNAME) @@ -188,16 +186,15 @@ def async_setup(hass, config): if motion is None: motion = 'motion_active' in cam.enabled_sensors - @asyncio.coroutine - def async_update_data(now): + async def async_update_data(now): """Update data from IP camera in SCAN_INTERVAL.""" - yield from cam.update() + await cam.update() async_dispatcher_send(hass, SIGNAL_UPDATE_DATA, host) async_track_point_in_utc_time( hass, async_update_data, utcnow() + interval) - yield from async_update_data(None) + await async_update_data(None) # Load platforms webcams[host] = cam @@ -242,7 +239,7 @@ def async_setup(hass, config): tasks = [async_setup_ipcamera(conf) for conf in config[DOMAIN]] if tasks: - yield from asyncio.wait(tasks, loop=hass.loop) + await asyncio.wait(tasks, loop=hass.loop) return True @@ -255,8 +252,7 @@ class AndroidIPCamEntity(Entity): self._host = host self._ipcam = ipcam - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Register update dispatcher.""" @callback def async_ipcam_update(host): diff --git a/homeassistant/components/apple_tv.py b/homeassistant/components/apple_tv.py index 21ff0e3286d..96e31e6bbf1 100644 --- a/homeassistant/components/apple_tv.py +++ b/homeassistant/components/apple_tv.py @@ -77,14 +77,13 @@ def request_configuration(hass, config, atv, credentials): """Request configuration steps from the user.""" configurator = hass.components.configurator - @asyncio.coroutine - def configuration_callback(callback_data): + async def configuration_callback(callback_data): """Handle the submitted configuration.""" from pyatv import exceptions pin = callback_data.get('pin') try: - yield from atv.airplay.finish_authentication(pin) + await atv.airplay.finish_authentication(pin) hass.components.persistent_notification.async_create( 'Authentication succeeded!

Add the following ' 'to credentials: in your apple_tv configuration:

' @@ -108,11 +107,10 @@ def request_configuration(hass, config, atv, credentials): ) -@asyncio.coroutine -def scan_for_apple_tvs(hass): +async def scan_for_apple_tvs(hass): """Scan for devices and present a notification of the ones found.""" import pyatv - atvs = yield from pyatv.scan_for_apple_tvs(hass.loop, timeout=3) + atvs = await pyatv.scan_for_apple_tvs(hass.loop, timeout=3) devices = [] for atv in atvs: @@ -132,14 +130,12 @@ def scan_for_apple_tvs(hass): notification_id=NOTIFICATION_SCAN_ID) -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Set up the Apple TV component.""" if DATA_APPLE_TV not in hass.data: hass.data[DATA_APPLE_TV] = {} - @asyncio.coroutine - def async_service_handler(service): + async def async_service_handler(service): """Handle service calls.""" entity_ids = service.data.get(ATTR_ENTITY_ID) @@ -158,17 +154,16 @@ def async_setup(hass, config): continue atv = device.atv - credentials = yield from atv.airplay.generate_credentials() - yield from atv.airplay.load_credentials(credentials) + credentials = await atv.airplay.generate_credentials() + await atv.airplay.load_credentials(credentials) _LOGGER.debug('Generated new credentials: %s', credentials) - yield from atv.airplay.start_authentication() + await atv.airplay.start_authentication() hass.async_add_job(request_configuration, hass, config, atv, credentials) - @asyncio.coroutine - def atv_discovered(service, info): + async def atv_discovered(service, info): """Set up an Apple TV that was auto discovered.""" - yield from _setup_atv(hass, { + await _setup_atv(hass, { CONF_NAME: info['name'], CONF_HOST: info['host'], CONF_LOGIN_ID: info['properties']['hG'], @@ -179,7 +174,7 @@ def async_setup(hass, config): tasks = [_setup_atv(hass, conf) for conf in config.get(DOMAIN, [])] if tasks: - yield from asyncio.wait(tasks, loop=hass.loop) + await asyncio.wait(tasks, loop=hass.loop) hass.services.async_register( DOMAIN, SERVICE_SCAN, async_service_handler, @@ -192,8 +187,7 @@ def async_setup(hass, config): return True -@asyncio.coroutine -def _setup_atv(hass, atv_config): +async def _setup_atv(hass, atv_config): """Set up an Apple TV.""" import pyatv name = atv_config.get(CONF_NAME) @@ -209,7 +203,7 @@ def _setup_atv(hass, atv_config): session = async_get_clientsession(hass) atv = pyatv.connect_to_apple_tv(details, hass.loop, session=session) if credentials: - yield from atv.airplay.load_credentials(credentials) + await atv.airplay.load_credentials(credentials) power = AppleTVPowerManager(hass, atv, start_off) hass.data[DATA_APPLE_TV][host] = { diff --git a/homeassistant/components/cloud/__init__.py b/homeassistant/components/cloud/__init__.py index 33a939bf9d0..217b39aff62 100644 --- a/homeassistant/components/cloud/__init__.py +++ b/homeassistant/components/cloud/__init__.py @@ -92,8 +92,7 @@ CONFIG_SCHEMA = vol.Schema({ }, extra=vol.ALLOW_EXTRA) -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Initialize the Home Assistant cloud.""" if DOMAIN in config: kwargs = dict(config[DOMAIN]) @@ -112,7 +111,7 @@ def async_setup(hass, config): cloud = hass.data[DOMAIN] = Cloud(hass, **kwargs) hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, cloud.async_start) - yield from http_api.async_setup(hass) + await http_api.async_setup(hass) return True @@ -226,17 +225,16 @@ class Cloud: 'authorization': self.id_token }) - @asyncio.coroutine - def logout(self): + async def logout(self): """Close connection and remove all credentials.""" - yield from self.iot.disconnect() + await self.iot.disconnect() self.id_token = None self.access_token = None self.refresh_token = None self._gactions_config = None - yield from self.hass.async_add_job( + await self.hass.async_add_job( lambda: os.remove(self.user_info_path)) def write_user_info(self): @@ -313,8 +311,7 @@ class Cloud: self._prefs[STORAGE_ENABLE_ALEXA] = alexa_enabled await self._store.async_save(self._prefs) - @asyncio.coroutine - def _fetch_jwt_keyset(self): + async def _fetch_jwt_keyset(self): """Fetch the JWT keyset for the Cognito instance.""" session = async_get_clientsession(self.hass) url = ("https://cognito-idp.us-east-1.amazonaws.com/" @@ -322,8 +319,8 @@ class Cloud: try: with async_timeout.timeout(10, loop=self.hass.loop): - req = yield from session.get(url) - self.jwt_keyset = yield from req.json() + req = await session.get(url) + self.jwt_keyset = await req.json() return True diff --git a/homeassistant/components/demo.py b/homeassistant/components/demo.py index c2c7866148f..8999087a137 100644 --- a/homeassistant/components/demo.py +++ b/homeassistant/components/demo.py @@ -35,8 +35,7 @@ COMPONENTS_WITH_DEMO_PLATFORM = [ ] -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Set up the demo environment.""" group = hass.components.group configurator = hass.components.configurator @@ -101,7 +100,7 @@ def async_setup(hass, config): {'weblink': {'entities': [{'name': 'Router', 'url': 'http://192.168.1.1'}]}})) - results = yield from asyncio.gather(*tasks, loop=hass.loop) + results = await asyncio.gather(*tasks, loop=hass.loop) if any(not result for result in results): return False @@ -192,7 +191,7 @@ def async_setup(hass, config): 'climate.ecobee', ], view=True)) - results = yield from asyncio.gather(*tasks2, loop=hass.loop) + results = await asyncio.gather(*tasks2, loop=hass.loop) if any(not result for result in results): return False diff --git a/homeassistant/components/device_sun_light_trigger.py b/homeassistant/components/device_sun_light_trigger.py index b766513f1f4..cd81b3a01ad 100644 --- a/homeassistant/components/device_sun_light_trigger.py +++ b/homeassistant/components/device_sun_light_trigger.py @@ -4,7 +4,6 @@ Provides functionality to turn on lights based on the states. For more details about this component, please refer to the documentation at https://home-assistant.io/components/device_sun_light_trigger/ """ -import asyncio import logging from datetime import timedelta @@ -45,8 +44,7 @@ CONFIG_SCHEMA = vol.Schema({ }, extra=vol.ALLOW_EXTRA) -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Set up the triggers to control lights based on device presence.""" logger = logging.getLogger(__name__) device_tracker = hass.components.device_tracker diff --git a/homeassistant/components/doorbird.py b/homeassistant/components/doorbird.py index c97289b9f07..ab929eb90bb 100644 --- a/homeassistant/components/doorbird.py +++ b/homeassistant/components/doorbird.py @@ -6,7 +6,6 @@ https://home-assistant.io/components/doorbird/ """ import logging -import asyncio import voluptuous as vol from homeassistant.components.http import HomeAssistantView @@ -170,8 +169,7 @@ class DoorbirdRequestView(HomeAssistantView): extra_urls = [API_URL + '/{sensor}'] # pylint: disable=no-self-use - @asyncio.coroutine - def get(self, request, sensor): + async def get(self, request, sensor): """Respond to requests from the device.""" hass = request.app['hass'] diff --git a/homeassistant/components/duckdns.py b/homeassistant/components/duckdns.py index 6ec2e6b1e03..3420bbed1bc 100644 --- a/homeassistant/components/duckdns.py +++ b/homeassistant/components/duckdns.py @@ -4,7 +4,6 @@ Integrate with DuckDNS. For more details about this component, please refer to the documentation at https://home-assistant.io/components/duckdns/ """ -import asyncio from datetime import timedelta import logging @@ -39,27 +38,24 @@ SERVICE_TXT_SCHEMA = vol.Schema({ }) -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Initialize the DuckDNS component.""" domain = config[DOMAIN][CONF_DOMAIN] token = config[DOMAIN][CONF_ACCESS_TOKEN] session = async_get_clientsession(hass) - result = yield from _update_duckdns(session, domain, token) + result = await _update_duckdns(session, domain, token) if not result: return False - @asyncio.coroutine - def update_domain_interval(now): + async def update_domain_interval(now): """Update the DuckDNS entry.""" - yield from _update_duckdns(session, domain, token) + await _update_duckdns(session, domain, token) - @asyncio.coroutine - def update_domain_service(call): + async def update_domain_service(call): """Update the DuckDNS entry.""" - yield from _update_duckdns( + await _update_duckdns( session, domain, token, txt=call.data[ATTR_TXT]) async_track_time_interval(hass, update_domain_interval, INTERVAL) @@ -73,8 +69,8 @@ def async_setup(hass, config): _SENTINEL = object() -@asyncio.coroutine -def _update_duckdns(session, domain, token, *, txt=_SENTINEL, clear=False): +async def _update_duckdns(session, domain, token, *, txt=_SENTINEL, + clear=False): """Update DuckDNS.""" params = { 'domains': domain, @@ -92,8 +88,8 @@ def _update_duckdns(session, domain, token, *, txt=_SENTINEL, clear=False): if clear: params['clear'] = 'true' - resp = yield from session.get(UPDATE_URL, params=params) - body = yield from resp.text() + resp = await session.get(UPDATE_URL, params=params) + body = await resp.text() if body != 'OK': _LOGGER.warning("Updating DuckDNS domain failed: %s", domain) diff --git a/homeassistant/components/emulated_hue/hue_api.py b/homeassistant/components/emulated_hue/hue_api.py index f7fbe2e15e3..fa24b656d7a 100644 --- a/homeassistant/components/emulated_hue/hue_api.py +++ b/homeassistant/components/emulated_hue/hue_api.py @@ -1,5 +1,4 @@ """Provides a Hue API to control Home Assistant.""" -import asyncio import logging from aiohttp import web @@ -36,11 +35,10 @@ class HueUsernameView(HomeAssistantView): extra_urls = ['/api/'] requires_auth = False - @asyncio.coroutine - def post(self, request): + async def post(self, request): """Handle a POST request.""" try: - data = yield from request.json() + data = await request.json() except ValueError: return self.json_message('Invalid JSON', HTTP_BAD_REQUEST) @@ -146,8 +144,7 @@ class HueOneLightChangeView(HomeAssistantView): """Initialize the instance of the view.""" self.config = config - @asyncio.coroutine - def put(self, request, username, entity_number): + async def put(self, request, username, entity_number): """Process a request to set the state of an individual light.""" config = self.config hass = request.app['hass'] @@ -168,7 +165,7 @@ class HueOneLightChangeView(HomeAssistantView): return web.Response(text="Entity not exposed", status=404) try: - request_json = yield from request.json() + request_json = await request.json() except ValueError: _LOGGER.error('Received invalid json') return self.json_message('Invalid JSON', HTTP_BAD_REQUEST) diff --git a/homeassistant/components/envisalink.py b/homeassistant/components/envisalink.py index 9b5b25c934c..eabe2d76851 100644 --- a/homeassistant/components/envisalink.py +++ b/homeassistant/components/envisalink.py @@ -81,8 +81,7 @@ CONFIG_SCHEMA = vol.Schema({ }, extra=vol.ALLOW_EXTRA) -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Set up for Envisalink devices.""" from pyenvisalink import EnvisalinkAlarmPanel @@ -165,7 +164,7 @@ def async_setup(hass, config): _LOGGER.info("Start envisalink.") controller.start() - result = yield from sync_connect + result = await sync_connect if not result: return False diff --git a/homeassistant/components/ffmpeg.py b/homeassistant/components/ffmpeg.py index 9aaae16ee21..64915f8849c 100644 --- a/homeassistant/components/ffmpeg.py +++ b/homeassistant/components/ffmpeg.py @@ -4,7 +4,6 @@ Component that will help set the FFmpeg component. For more details about this component, please refer to the documentation at https://home-assistant.io/components/ffmpeg/ """ -import asyncio import logging import voluptuous as vol @@ -76,8 +75,7 @@ def async_restart(hass, entity_id=None): hass.async_add_job(hass.services.async_call(DOMAIN, SERVICE_RESTART, data)) -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Set up the FFmpeg component.""" conf = config.get(DOMAIN, {}) @@ -88,8 +86,7 @@ def async_setup(hass, config): ) # Register service - @asyncio.coroutine - def async_service_handle(service): + async def async_service_handle(service): """Handle service ffmpeg process.""" entity_ids = service.data.get(ATTR_ENTITY_ID) @@ -131,8 +128,7 @@ class FFmpegManager: """Return ffmpeg binary from config.""" return self._bin - @asyncio.coroutine - def async_run_test(self, input_source): + async def async_run_test(self, input_source): """Run test on this input. TRUE is deactivate or run correct. This method must be run in the event loop. @@ -146,7 +142,7 @@ class FFmpegManager: # run test ffmpeg_test = Test(self.binary, loop=self.hass.loop) - success = yield from ffmpeg_test.run_test(input_source) + success = await ffmpeg_test.run_test(input_source) if not success: _LOGGER.error("FFmpeg '%s' test fails!", input_source) self._cache[input_source] = False @@ -163,8 +159,7 @@ class FFmpegBase(Entity): self.ffmpeg = None self.initial_state = initial_state - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Register dispatcher & events. This method is a coroutine. @@ -189,40 +184,36 @@ class FFmpegBase(Entity): """Return True if entity has to be polled for state.""" return False - @asyncio.coroutine - def _async_start_ffmpeg(self, entity_ids): + async def _async_start_ffmpeg(self, entity_ids): """Start a FFmpeg process. This method is a coroutine. """ raise NotImplementedError() - @asyncio.coroutine - def _async_stop_ffmpeg(self, entity_ids): + async def _async_stop_ffmpeg(self, entity_ids): """Stop a FFmpeg process. This method is a coroutine. """ if entity_ids is None or self.entity_id in entity_ids: - yield from self.ffmpeg.close() + await self.ffmpeg.close() - @asyncio.coroutine - def _async_restart_ffmpeg(self, entity_ids): + async def _async_restart_ffmpeg(self, entity_ids): """Stop a FFmpeg process. This method is a coroutine. """ if entity_ids is None or self.entity_id in entity_ids: - yield from self._async_stop_ffmpeg(None) - yield from self._async_start_ffmpeg(None) + await self._async_stop_ffmpeg(None) + await self._async_start_ffmpeg(None) @callback def _async_register_events(self): """Register a FFmpeg process/device.""" - @asyncio.coroutine - def async_shutdown_handle(event): + async def async_shutdown_handle(event): """Stop FFmpeg process.""" - yield from self._async_stop_ffmpeg(None) + await self._async_stop_ffmpeg(None) self.hass.bus.async_listen_once( EVENT_HOMEASSISTANT_STOP, async_shutdown_handle) @@ -231,10 +222,9 @@ class FFmpegBase(Entity): if not self.initial_state: return - @asyncio.coroutine - def async_start_handle(event): + async def async_start_handle(event): """Start FFmpeg process.""" - yield from self._async_start_ffmpeg(None) + await self._async_start_ffmpeg(None) self.async_schedule_update_ha_state() self.hass.bus.async_listen_once( diff --git a/homeassistant/components/foursquare.py b/homeassistant/components/foursquare.py index 2c10df327f4..a4a7395adc4 100644 --- a/homeassistant/components/foursquare.py +++ b/homeassistant/components/foursquare.py @@ -4,7 +4,6 @@ Support for the Foursquare (Swarm) API. For more details about this component, please refer to the documentation at https://home-assistant.io/components/foursquare/ """ -import asyncio import logging import requests @@ -85,11 +84,10 @@ class FoursquarePushReceiver(HomeAssistantView): """Initialize the OAuth callback view.""" self.push_secret = push_secret - @asyncio.coroutine - def post(self, request): + async def post(self, request): """Accept the POST from Foursquare.""" try: - data = yield from request.json() + data = await request.json() except ValueError: return self.json_message('Invalid JSON', HTTP_BAD_REQUEST) diff --git a/homeassistant/components/freedns.py b/homeassistant/components/freedns.py index 0512030bdcb..0b5cbeda01a 100644 --- a/homeassistant/components/freedns.py +++ b/homeassistant/components/freedns.py @@ -37,8 +37,7 @@ CONFIG_SCHEMA = vol.Schema({ }, extra=vol.ALLOW_EXTRA) -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Initialize the FreeDNS component.""" url = config[DOMAIN].get(CONF_URL) auth_token = config[DOMAIN].get(CONF_ACCESS_TOKEN) @@ -46,16 +45,15 @@ def async_setup(hass, config): session = hass.helpers.aiohttp_client.async_get_clientsession() - result = yield from _update_freedns( + result = await _update_freedns( hass, session, url, auth_token) if result is False: return False - @asyncio.coroutine - def update_domain_callback(now): + async def update_domain_callback(now): """Update the FreeDNS entry.""" - yield from _update_freedns(hass, session, url, auth_token) + await _update_freedns(hass, session, url, auth_token) hass.helpers.event.async_track_time_interval( update_domain_callback, update_interval) @@ -63,8 +61,7 @@ def async_setup(hass, config): return True -@asyncio.coroutine -def _update_freedns(hass, session, url, auth_token): +async def _update_freedns(hass, session, url, auth_token): """Update FreeDNS.""" params = None @@ -77,8 +74,8 @@ def _update_freedns(hass, session, url, auth_token): try: with async_timeout.timeout(TIMEOUT, loop=hass.loop): - resp = yield from session.get(url, params=params) - body = yield from resp.text() + resp = await session.get(url, params=params) + body = await resp.text() if "has not changed" in body: # IP has not changed. diff --git a/homeassistant/components/google_domains.py b/homeassistant/components/google_domains.py index 3b414306be5..32bdb79557a 100644 --- a/homeassistant/components/google_domains.py +++ b/homeassistant/components/google_domains.py @@ -36,8 +36,7 @@ CONFIG_SCHEMA = vol.Schema({ }, extra=vol.ALLOW_EXTRA) -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Initialize the Google Domains component.""" domain = config[DOMAIN].get(CONF_DOMAIN) user = config[DOMAIN].get(CONF_USERNAME) @@ -46,16 +45,15 @@ def async_setup(hass, config): session = hass.helpers.aiohttp_client.async_get_clientsession() - result = yield from _update_google_domains( + result = await _update_google_domains( hass, session, domain, user, password, timeout) if not result: return False - @asyncio.coroutine - def update_domain_interval(now): + async def update_domain_interval(now): """Update the Google Domains entry.""" - yield from _update_google_domains( + await _update_google_domains( hass, session, domain, user, password, timeout) hass.helpers.event.async_track_time_interval( @@ -64,8 +62,8 @@ def async_setup(hass, config): return True -@asyncio.coroutine -def _update_google_domains(hass, session, domain, user, password, timeout): +async def _update_google_domains(hass, session, domain, user, password, + timeout): """Update Google Domains.""" url = UPDATE_URL.format(user, password) @@ -75,8 +73,8 @@ def _update_google_domains(hass, session, domain, user, password, timeout): try: with async_timeout.timeout(timeout, loop=hass.loop): - resp = yield from session.get(url, params=params) - body = yield from resp.text() + resp = await session.get(url, params=params) + body = await resp.text() if body.startswith('good') or body.startswith('nochg'): return True diff --git a/homeassistant/components/homematic/__init__.py b/homeassistant/components/homematic/__init__.py index 3d6eb69bb5e..b2b3b18ff34 100644 --- a/homeassistant/components/homematic/__init__.py +++ b/homeassistant/components/homematic/__init__.py @@ -4,7 +4,6 @@ Support for HomeMatic devices. For more details about this component, please refer to the documentation at https://home-assistant.io/components/homematic/ """ -import asyncio from datetime import timedelta from functools import partial import logging @@ -715,10 +714,9 @@ class HMDevice(Entity): if self._state: self._state = self._state.upper() - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Load data init callbacks.""" - yield from self.hass.async_add_job(self.link_homematic) + await self.hass.async_add_job(self.link_homematic) @property def unique_id(self): diff --git a/homeassistant/components/hydrawise.py b/homeassistant/components/hydrawise.py index 0c4db63034e..5a045a083b3 100644 --- a/homeassistant/components/hydrawise.py +++ b/homeassistant/components/hydrawise.py @@ -4,7 +4,6 @@ Support for Hydrawise cloud. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/hydrawise/ """ -import asyncio from datetime import timedelta import logging @@ -127,8 +126,7 @@ class HydrawiseEntity(Entity): """Return the name of the sensor.""" return self._name - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Register callbacks.""" async_dispatcher_connect( self.hass, SIGNAL_UPDATE_HYDRAWISE, self._update_callback) diff --git a/homeassistant/components/ihc/ihcdevice.py b/homeassistant/components/ihc/ihcdevice.py index 93ab81850c9..26ee2fb14fc 100644 --- a/homeassistant/components/ihc/ihcdevice.py +++ b/homeassistant/components/ihc/ihcdevice.py @@ -1,5 +1,4 @@ """Implementation of a base class for all IHC devices.""" -import asyncio from homeassistant.helpers.entity import Entity @@ -28,8 +27,7 @@ class IHCDevice(Entity): self.ihc_note = '' self.ihc_position = '' - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Add callback for IHC changes.""" self.ihc_controller.add_notify_event( self.ihc_id, self.on_ihc_change, True) diff --git a/homeassistant/components/insteon/__init__.py b/homeassistant/components/insteon/__init__.py index 749d167e6de..924baeaa560 100644 --- a/homeassistant/components/insteon/__init__.py +++ b/homeassistant/components/insteon/__init__.py @@ -4,7 +4,6 @@ Support for INSTEON Modems (PLM and Hub). For more details about this component, please refer to the documentation at https://home-assistant.io/components/insteon/ """ -import asyncio import collections import logging from typing import Dict @@ -149,8 +148,7 @@ X10_HOUSECODE_SCHEMA = vol.Schema({ }) -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Set up the connection to the modem.""" import insteonplm @@ -292,7 +290,7 @@ def async_setup(hass, config): if host: _LOGGER.info('Connecting to Insteon Hub on %s', host) - conn = yield from insteonplm.Connection.create( + conn = await insteonplm.Connection.create( host=host, port=ip_port, username=username, @@ -302,7 +300,7 @@ def async_setup(hass, config): workdir=hass.config.config_dir) else: _LOGGER.info("Looking for Insteon PLM on %s", port) - conn = yield from insteonplm.Connection.create( + conn = await insteonplm.Connection.create( device=port, loop=hass.loop, workdir=hass.config.config_dir) @@ -494,8 +492,7 @@ class InsteonEntity(Entity): deviceid.human, group, val) self.async_schedule_update_ha_state() - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Register INSTEON update events.""" _LOGGER.debug('Tracking updates for device %s group %d statename %s', self.address, self.group, diff --git a/homeassistant/components/insteon_plm.py b/homeassistant/components/insteon_plm.py index b89e5679a63..b3011e9d7bd 100644 --- a/homeassistant/components/insteon_plm.py +++ b/homeassistant/components/insteon_plm.py @@ -4,14 +4,12 @@ Support for INSTEON PowerLinc Modem. For more details about this component, please refer to the documentation at https://home-assistant.io/components/insteon_plm/ """ -import asyncio import logging _LOGGER = logging.getLogger(__name__) -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Set up the insteon_plm component. This component is deprecated as of release 0.77 and should be removed in diff --git a/homeassistant/components/intent_script.py b/homeassistant/components/intent_script.py index 91489e188c5..0c47b8880ba 100644 --- a/homeassistant/components/intent_script.py +++ b/homeassistant/components/intent_script.py @@ -1,5 +1,4 @@ """Handle intents with scripts.""" -import asyncio import copy import logging @@ -45,8 +44,7 @@ CONFIG_SCHEMA = vol.Schema({ _LOGGER = logging.getLogger(__name__) -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Activate Alexa component.""" intents = copy.deepcopy(config[DOMAIN]) template.attach(hass, intents) @@ -69,8 +67,7 @@ class ScriptIntentHandler(intent.IntentHandler): self.intent_type = intent_type self.config = config - @asyncio.coroutine - def async_handle(self, intent_obj): + async def async_handle(self, intent_obj): """Handle the intent.""" speech = self.config.get(CONF_SPEECH) card = self.config.get(CONF_CARD) @@ -83,7 +80,7 @@ class ScriptIntentHandler(intent.IntentHandler): if is_async_action: intent_obj.hass.async_add_job(action.async_run(slots)) else: - yield from action.async_run(slots) + await action.async_run(slots) response = intent_obj.create_response() diff --git a/homeassistant/components/introduction.py b/homeassistant/components/introduction.py index cc3e00c4475..17de7fcd6ca 100644 --- a/homeassistant/components/introduction.py +++ b/homeassistant/components/introduction.py @@ -4,7 +4,6 @@ Component that will help guide the user taking its first steps. For more details about this component, please refer to the documentation at https://home-assistant.io/components/introduction/ """ -import asyncio import logging import voluptuous as vol @@ -16,8 +15,7 @@ CONFIG_SCHEMA = vol.Schema({ }, extra=vol.ALLOW_EXTRA) -@asyncio.coroutine -def async_setup(hass, config=None): +async def async_setup(hass, config=None): """Set up the introduction component.""" log = logging.getLogger(__name__) log.info(""" diff --git a/homeassistant/components/ios/__init__.py b/homeassistant/components/ios/__init__.py index a67be0a63de..0b1282b605a 100644 --- a/homeassistant/components/ios/__init__.py +++ b/homeassistant/components/ios/__init__.py @@ -4,7 +4,6 @@ Native Home Assistant iOS app component. For more details about this component, please refer to the documentation at https://home-assistant.io/ecosystem/ios/ """ -import asyncio import logging import datetime @@ -259,11 +258,10 @@ class iOSIdentifyDeviceView(HomeAssistantView): """Initiliaze the view.""" self._config_path = config_path - @asyncio.coroutine - def post(self, request): + async def post(self, request): """Handle the POST request for device identification.""" try: - data = yield from request.json() + data = await request.json() except ValueError: return self.json_message("Invalid JSON", HTTP_BAD_REQUEST) diff --git a/homeassistant/components/isy994.py b/homeassistant/components/isy994.py index d8afb7be5da..9b539b0690a 100644 --- a/homeassistant/components/isy994.py +++ b/homeassistant/components/isy994.py @@ -4,7 +4,6 @@ Support the ISY-994 controllers. For configuration details please visit the documentation for this component at https://home-assistant.io/components/isy994/ """ -import asyncio from collections import namedtuple import logging from urllib.parse import urlparse @@ -414,8 +413,7 @@ class ISYDevice(Entity): self._change_handler = None self._control_handler = None - @asyncio.coroutine - def async_added_to_hass(self) -> None: + async def async_added_to_hass(self) -> None: """Subscribe to the node change events.""" self._change_handler = self._node.status.subscribe( 'changed', self.on_update) diff --git a/homeassistant/components/lutron.py b/homeassistant/components/lutron.py index bef821220b3..2e49d7ce690 100644 --- a/homeassistant/components/lutron.py +++ b/homeassistant/components/lutron.py @@ -4,7 +4,6 @@ Component for interacting with a Lutron RadioRA 2 system. For more details about this component, please refer to the documentation at https://home-assistant.io/components/lutron/ """ -import asyncio import logging import voluptuous as vol @@ -69,8 +68,7 @@ class LutronDevice(Entity): self._controller = controller self._area_name = area_name - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Register callbacks.""" self.hass.async_add_job( self._controller.subscribe, self._lutron_device, diff --git a/homeassistant/components/lutron_caseta.py b/homeassistant/components/lutron_caseta.py index 2535fb76120..eb4010e43a1 100644 --- a/homeassistant/components/lutron_caseta.py +++ b/homeassistant/components/lutron_caseta.py @@ -4,7 +4,6 @@ Component for interacting with a Lutron Caseta system. For more details about this component, please refer to the documentation at https://home-assistant.io/components/lutron_caseta/ """ -import asyncio import logging import voluptuous as vol @@ -40,8 +39,7 @@ LUTRON_CASETA_COMPONENTS = [ ] -@asyncio.coroutine -def async_setup(hass, base_config): +async def async_setup(hass, base_config): """Set up the Lutron component.""" from pylutron_caseta.smartbridge import Smartbridge @@ -54,7 +52,7 @@ def async_setup(hass, base_config): certfile=certfile, ca_certs=ca_certs) hass.data[LUTRON_CASETA_SMARTBRIDGE] = bridge - yield from bridge.connect() + await bridge.connect() if not hass.data[LUTRON_CASETA_SMARTBRIDGE].is_connected(): _LOGGER.error("Unable to connect to Lutron smartbridge at %s", config[CONF_HOST]) @@ -85,8 +83,7 @@ class LutronCasetaDevice(Entity): self._state = None self._smartbridge = bridge - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Register callbacks.""" self._smartbridge.add_subscriber(self._device_id, self.async_schedule_update_ha_state) diff --git a/homeassistant/components/microsoft_face.py b/homeassistant/components/microsoft_face.py index e0e0e716d2e..c06ed6bc8f3 100644 --- a/homeassistant/components/microsoft_face.py +++ b/homeassistant/components/microsoft_face.py @@ -106,8 +106,7 @@ def face_person(hass, group, person, camera_entity): hass.services.call(DOMAIN, SERVICE_FACE_PERSON, data) -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Set up Microsoft Face.""" entities = {} face = MicrosoftFace( @@ -120,26 +119,25 @@ def async_setup(hass, config): try: # read exists group/person from cloud and create entities - yield from face.update_store() + await face.update_store() except HomeAssistantError as err: _LOGGER.error("Can't load data from face api: %s", err) return False hass.data[DATA_MICROSOFT_FACE] = face - @asyncio.coroutine - def async_create_group(service): + async def async_create_group(service): """Create a new person group.""" name = service.data[ATTR_NAME] g_id = slugify(name) try: - yield from face.call_api( + await face.call_api( 'put', "persongroups/{0}".format(g_id), {'name': name}) face.store[g_id] = {} entities[g_id] = MicrosoftFaceGroupEntity(hass, face, g_id, name) - yield from entities[g_id].async_update_ha_state() + await entities[g_id].async_update_ha_state() except HomeAssistantError as err: _LOGGER.error("Can't create group '%s' with error: %s", g_id, err) @@ -147,13 +145,12 @@ def async_setup(hass, config): DOMAIN, SERVICE_CREATE_GROUP, async_create_group, schema=SCHEMA_GROUP_SERVICE) - @asyncio.coroutine - def async_delete_group(service): + async def async_delete_group(service): """Delete a person group.""" g_id = slugify(service.data[ATTR_NAME]) try: - yield from face.call_api('delete', "persongroups/{0}".format(g_id)) + await face.call_api('delete', "persongroups/{0}".format(g_id)) face.store.pop(g_id) entity = entities.pop(g_id) @@ -165,13 +162,12 @@ def async_setup(hass, config): DOMAIN, SERVICE_DELETE_GROUP, async_delete_group, schema=SCHEMA_GROUP_SERVICE) - @asyncio.coroutine - def async_train_group(service): + async def async_train_group(service): """Train a person group.""" g_id = service.data[ATTR_GROUP] try: - yield from face.call_api( + await face.call_api( 'post', "persongroups/{0}/train".format(g_id)) except HomeAssistantError as err: _LOGGER.error("Can't train group '%s' with error: %s", g_id, err) @@ -180,19 +176,18 @@ def async_setup(hass, config): DOMAIN, SERVICE_TRAIN_GROUP, async_train_group, schema=SCHEMA_TRAIN_SERVICE) - @asyncio.coroutine - def async_create_person(service): + async def async_create_person(service): """Create a person in a group.""" name = service.data[ATTR_NAME] g_id = service.data[ATTR_GROUP] try: - user_data = yield from face.call_api( + user_data = await face.call_api( 'post', "persongroups/{0}/persons".format(g_id), {'name': name} ) face.store[g_id][name] = user_data['personId'] - yield from entities[g_id].async_update_ha_state() + await entities[g_id].async_update_ha_state() except HomeAssistantError as err: _LOGGER.error("Can't create person '%s' with error: %s", name, err) @@ -200,19 +195,18 @@ def async_setup(hass, config): DOMAIN, SERVICE_CREATE_PERSON, async_create_person, schema=SCHEMA_PERSON_SERVICE) - @asyncio.coroutine - def async_delete_person(service): + async def async_delete_person(service): """Delete a person in a group.""" name = service.data[ATTR_NAME] g_id = service.data[ATTR_GROUP] p_id = face.store[g_id].get(name) try: - yield from face.call_api( + await face.call_api( 'delete', "persongroups/{0}/persons/{1}".format(g_id, p_id)) face.store[g_id].pop(name) - yield from entities[g_id].async_update_ha_state() + await entities[g_id].async_update_ha_state() except HomeAssistantError as err: _LOGGER.error("Can't delete person '%s' with error: %s", p_id, err) @@ -220,8 +214,7 @@ def async_setup(hass, config): DOMAIN, SERVICE_DELETE_PERSON, async_delete_person, schema=SCHEMA_PERSON_SERVICE) - @asyncio.coroutine - def async_face_person(service): + async def async_face_person(service): """Add a new face picture to a person.""" g_id = service.data[ATTR_GROUP] p_id = face.store[g_id].get(service.data[ATTR_PERSON]) @@ -230,9 +223,9 @@ def async_setup(hass, config): camera = hass.components.camera try: - image = yield from camera.async_get_image(hass, camera_entity) + image = await camera.async_get_image(hass, camera_entity) - yield from face.call_api( + await face.call_api( 'post', "persongroups/{0}/persons/{1}/persistedFaces".format( g_id, p_id), @@ -307,10 +300,9 @@ class MicrosoftFace: """Store group/person data and IDs.""" return self._store - @asyncio.coroutine - def update_store(self): + async def update_store(self): """Load all group/person data into local store.""" - groups = yield from self.call_api('get', 'persongroups') + groups = await self.call_api('get', 'persongroups') tasks = [] for group in groups: @@ -319,7 +311,7 @@ class MicrosoftFace: self._entities[g_id] = MicrosoftFaceGroupEntity( self.hass, self, g_id, group['name']) - persons = yield from self.call_api( + persons = await self.call_api( 'get', "persongroups/{0}/persons".format(g_id)) for person in persons: @@ -328,11 +320,10 @@ class MicrosoftFace: tasks.append(self._entities[g_id].async_update_ha_state()) if tasks: - yield from asyncio.wait(tasks, loop=self.hass.loop) + await asyncio.wait(tasks, loop=self.hass.loop) - @asyncio.coroutine - def call_api(self, method, function, data=None, binary=False, - params=None): + async def call_api(self, method, function, data=None, binary=False, + params=None): """Make an api call.""" headers = {"Ocp-Apim-Subscription-Key": self._api_key} url = self._server_url.format(function) @@ -350,10 +341,10 @@ class MicrosoftFace: try: with async_timeout.timeout(self.timeout, loop=self.hass.loop): - response = yield from getattr(self.websession, method)( + response = await getattr(self.websession, method)( url, data=payload, headers=headers, params=params) - answer = yield from response.json() + answer = await response.json() _LOGGER.debug("Read from microsoft face api: %s", answer) if response.status < 300: diff --git a/homeassistant/components/mqtt_statestream.py b/homeassistant/components/mqtt_statestream.py index 592e31cbff1..3a0e5d39ff0 100644 --- a/homeassistant/components/mqtt_statestream.py +++ b/homeassistant/components/mqtt_statestream.py @@ -4,7 +4,6 @@ Publish simple item state changes via MQTT. For more details about this component, please refer to the documentation at https://home-assistant.io/components/mqtt_statestream/ """ -import asyncio import json import voluptuous as vol @@ -43,8 +42,7 @@ CONFIG_SCHEMA = vol.Schema({ }, extra=vol.ALLOW_EXTRA) -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Set up the MQTT state feed.""" conf = config.get(DOMAIN, {}) base_topic = conf.get(CONF_BASE_TOPIC) diff --git a/homeassistant/components/namecheapdns.py b/homeassistant/components/namecheapdns.py index dcca8829535..32a5c318852 100644 --- a/homeassistant/components/namecheapdns.py +++ b/homeassistant/components/namecheapdns.py @@ -4,7 +4,6 @@ Integrate with namecheap DNS services. For more details about this component, please refer to the documentation at https://home-assistant.io/components/namecheapdns/ """ -import asyncio import logging from datetime import timedelta @@ -32,8 +31,7 @@ CONFIG_SCHEMA = vol.Schema({ }, extra=vol.ALLOW_EXTRA) -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Initialize the namecheap DNS component.""" host = config[DOMAIN][CONF_HOST] domain = config[DOMAIN][CONF_DOMAIN] @@ -41,23 +39,21 @@ def async_setup(hass, config): session = async_get_clientsession(hass) - result = yield from _update_namecheapdns(session, host, domain, password) + result = await _update_namecheapdns(session, host, domain, password) if not result: return False - @asyncio.coroutine - def update_domain_interval(now): + async def update_domain_interval(now): """Update the namecheap DNS entry.""" - yield from _update_namecheapdns(session, host, domain, password) + await _update_namecheapdns(session, host, domain, password) async_track_time_interval(hass, update_domain_interval, INTERVAL) return result -@asyncio.coroutine -def _update_namecheapdns(session, host, domain, password): +async def _update_namecheapdns(session, host, domain, password): """Update namecheap DNS entry.""" import xml.etree.ElementTree as ET @@ -67,8 +63,8 @@ def _update_namecheapdns(session, host, domain, password): 'password': password, } - resp = yield from session.get(UPDATE_URL, params=params) - xml_string = yield from resp.text() + resp = await session.get(UPDATE_URL, params=params) + xml_string = await resp.text() root = ET.fromstring(xml_string) err_count = root.find('ErrCount').text diff --git a/homeassistant/components/no_ip.py b/homeassistant/components/no_ip.py index 6051fa85f55..beb11ed738f 100644 --- a/homeassistant/components/no_ip.py +++ b/homeassistant/components/no_ip.py @@ -53,8 +53,7 @@ CONFIG_SCHEMA = vol.Schema({ }, extra=vol.ALLOW_EXTRA) -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Initialize the NO-IP component.""" domain = config[DOMAIN].get(CONF_DOMAIN) user = config[DOMAIN].get(CONF_USERNAME) @@ -65,16 +64,15 @@ def async_setup(hass, config): session = hass.helpers.aiohttp_client.async_get_clientsession() - result = yield from _update_no_ip( + result = await _update_no_ip( hass, session, domain, auth_str, timeout) if not result: return False - @asyncio.coroutine - def update_domain_interval(now): + async def update_domain_interval(now): """Update the NO-IP entry.""" - yield from _update_no_ip(hass, session, domain, auth_str, timeout) + await _update_no_ip(hass, session, domain, auth_str, timeout) hass.helpers.event.async_track_time_interval( update_domain_interval, INTERVAL) @@ -82,8 +80,7 @@ def async_setup(hass, config): return True -@asyncio.coroutine -def _update_no_ip(hass, session, domain, auth_str, timeout): +async def _update_no_ip(hass, session, domain, auth_str, timeout): """Update NO-IP.""" url = UPDATE_URL @@ -98,8 +95,8 @@ def _update_no_ip(hass, session, domain, auth_str, timeout): try: with async_timeout.timeout(timeout, loop=hass.loop): - resp = yield from session.get(url, params=params, headers=headers) - body = yield from resp.text() + resp = await session.get(url, params=params, headers=headers) + body = await resp.text() if body.startswith('good') or body.startswith('nochg'): return True diff --git a/homeassistant/components/persistent_notification/__init__.py b/homeassistant/components/persistent_notification/__init__.py index 6b8fd68bc26..066afe1fe22 100644 --- a/homeassistant/components/persistent_notification/__init__.py +++ b/homeassistant/components/persistent_notification/__init__.py @@ -4,7 +4,6 @@ A component which is collecting configuration errors. For more details about this component, please refer to the documentation at https://home-assistant.io/components/persistent_notification/ """ -import asyncio import logging from collections import OrderedDict from typing import Awaitable @@ -99,8 +98,7 @@ def async_dismiss(hass: HomeAssistant, notification_id: str) -> None: hass.async_add_job(hass.services.async_call(DOMAIN, SERVICE_DISMISS, data)) -@asyncio.coroutine -def async_setup(hass: HomeAssistant, config: dict) -> Awaitable[bool]: +async def async_setup(hass: HomeAssistant, config: dict) -> Awaitable[bool]: """Set up the persistent notification component.""" persistent_notifications = OrderedDict() hass.data[DOMAIN] = {'notifications': persistent_notifications} diff --git a/homeassistant/components/plant.py b/homeassistant/components/plant.py index 84dc8402742..9659fd4f7e1 100644 --- a/homeassistant/components/plant.py +++ b/homeassistant/components/plant.py @@ -4,7 +4,6 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/plant/ """ import logging -import asyncio from datetime import datetime, timedelta from collections import deque import voluptuous as vol @@ -97,8 +96,7 @@ CONFIG_SCHEMA = vol.Schema({ ENABLE_LOAD_HISTORY = False -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Set up the Plant component.""" component = EntityComponent(_LOGGER, DOMAIN, hass, group_name=GROUP_NAME_ALL_PLANTS) @@ -112,7 +110,7 @@ def async_setup(hass, config): async_track_state_change(hass, sensor_entity_ids, entity.state_changed) entities.append(entity) - yield from component.async_add_entities(entities) + await component.async_add_entities(entities) return True @@ -246,15 +244,13 @@ class Plant(Entity): return '{} high'.format(sensor_name) return None - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """After being added to hass, load from history.""" if ENABLE_LOAD_HISTORY and 'recorder' in self.hass.config.components: # only use the database if it's configured self.hass.async_add_job(self._load_history_from_db) - @asyncio.coroutine - def _load_history_from_db(self): + async def _load_history_from_db(self): """Load the history of the brightness values from the database. This only needs to be done once during startup. diff --git a/homeassistant/components/prometheus.py b/homeassistant/components/prometheus.py index 5fa768b6983..ee4b88d4d9b 100644 --- a/homeassistant/components/prometheus.py +++ b/homeassistant/components/prometheus.py @@ -4,7 +4,6 @@ Support for Prometheus metrics export. For more details about this component, please refer to the documentation at https://home-assistant.io/components/prometheus/ """ -import asyncio import logging import voluptuous as vol @@ -265,8 +264,7 @@ class PrometheusView(HomeAssistantView): """Initialize Prometheus view.""" self.prometheus_client = prometheus_client - @asyncio.coroutine - def get(self, request): + async def get(self, request): """Handle request for Prometheus metrics.""" _LOGGER.debug("Received Prometheus metrics request") diff --git a/homeassistant/components/raincloud.py b/homeassistant/components/raincloud.py index 53cd8e79d7e..47f6176d5f8 100644 --- a/homeassistant/components/raincloud.py +++ b/homeassistant/components/raincloud.py @@ -4,7 +4,6 @@ Support for Melnor RainCloud sprinkler water timer. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/raincloud/ """ -import asyncio from datetime import timedelta import logging @@ -148,8 +147,7 @@ class RainCloudEntity(Entity): """Return the name of the sensor.""" return self._name - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Register callbacks.""" async_dispatcher_connect( self.hass, SIGNAL_UPDATE_RAINCLOUD, self._update_callback) diff --git a/homeassistant/components/rest_command.py b/homeassistant/components/rest_command.py index 4632315b757..3f9b258634d 100644 --- a/homeassistant/components/rest_command.py +++ b/homeassistant/components/rest_command.py @@ -53,8 +53,7 @@ CONFIG_SCHEMA = vol.Schema({ }, extra=vol.ALLOW_EXTRA) -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Set up the REST command component.""" websession = async_get_clientsession(hass) @@ -87,8 +86,7 @@ def async_setup(hass, config): headers = {} headers[hdrs.CONTENT_TYPE] = content_type - @asyncio.coroutine - def async_service_handler(service): + async def async_service_handler(service): """Execute a shell command service.""" payload = None if template_payload: @@ -98,7 +96,7 @@ def async_setup(hass, config): try: with async_timeout.timeout(timeout, loop=hass.loop): - request = yield from getattr(websession, method)( + request = await getattr(websession, method)( template_url.async_render(variables=service.data), data=payload, auth=auth, diff --git a/homeassistant/components/rflink.py b/homeassistant/components/rflink.py index b8af971b3ff..f96ce3968c4 100644 --- a/homeassistant/components/rflink.py +++ b/homeassistant/components/rflink.py @@ -105,8 +105,7 @@ def identify_event_type(event): return 'unknown' -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Set up the Rflink component.""" from rflink.protocol import create_rflink_connection import serial @@ -125,11 +124,10 @@ def async_setup(hass, config): # Allow platform to specify function to register new unknown devices hass.data[DATA_DEVICE_REGISTER] = {} - @asyncio.coroutine - def async_send_command(call): + async def async_send_command(call): """Send Rflink command.""" _LOGGER.debug('Rflink command for %s', str(call.data)) - if not (yield from RflinkCommand.send_command( + if not (await RflinkCommand.send_command( call.data.get(CONF_DEVICE_ID), call.data.get(CONF_COMMAND))): _LOGGER.error('Failed Rflink command for %s', str(call.data)) @@ -196,8 +194,7 @@ def async_setup(hass, config): _LOGGER.warning('disconnected from Rflink, reconnecting') hass.async_add_job(connect) - @asyncio.coroutine - def connect(): + async def connect(): """Set up connection and hook it into HA for reconnect/shutdown.""" _LOGGER.info('Initiating Rflink connection') @@ -217,7 +214,7 @@ def async_setup(hass, config): try: with async_timeout.timeout(CONNECTION_TIMEOUT, loop=hass.loop): - transport, protocol = yield from connection + transport, protocol = await connection except (serial.serialutil.SerialException, ConnectionRefusedError, TimeoutError, OSError, asyncio.TimeoutError) as exc: @@ -330,8 +327,7 @@ class RflinkDevice(Entity): self._available = availability self.async_schedule_update_ha_state() - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Register update callback.""" async_dispatcher_connect(self.hass, SIGNAL_AVAILABILITY, self.set_availability) @@ -367,13 +363,11 @@ class RflinkCommand(RflinkDevice): return bool(cls._protocol) @classmethod - @asyncio.coroutine - def send_command(cls, device_id, action): + async def send_command(cls, device_id, action): """Send device command to Rflink and wait for acknowledgement.""" - return (yield from cls._protocol.send_command_ack(device_id, action)) + return await cls._protocol.send_command_ack(device_id, action) - @asyncio.coroutine - def _async_handle_command(self, command, *args): + async def _async_handle_command(self, command, *args): """Do bookkeeping for command, send it to rflink and update state.""" self.cancel_queued_send_commands() @@ -412,10 +406,10 @@ class RflinkCommand(RflinkDevice): # Send initial command and queue repetitions. # This allows the entity state to be updated quickly and not having to # wait for all repetitions to be sent - yield from self._async_send_command(cmd, self._signal_repetitions) + await self._async_send_command(cmd, self._signal_repetitions) # Update state of entity - yield from self.async_update_ha_state() + await self.async_update_ha_state() def cancel_queued_send_commands(self): """Cancel queued signal repetition commands. @@ -428,8 +422,7 @@ class RflinkCommand(RflinkDevice): if self._repetition_task: self._repetition_task.cancel() - @asyncio.coroutine - def _async_send_command(self, cmd, repetitions): + async def _async_send_command(self, cmd, repetitions): """Send a command for device to Rflink gateway.""" _LOGGER.debug( "Sending command: %s to Rflink device: %s", cmd, self._device_id) @@ -440,7 +433,7 @@ class RflinkCommand(RflinkDevice): if self._wait_ack: # Puts command on outgoing buffer then waits for Rflink to confirm # the command has been send out in the ether. - yield from self._protocol.send_command_ack(self._device_id, cmd) + await self._protocol.send_command_ack(self._device_id, cmd) else: # Puts command on outgoing buffer and returns straight away. # Rflink protocol/transport handles asynchronous writing of buffer diff --git a/homeassistant/components/rfxtrx.py b/homeassistant/components/rfxtrx.py index b5bc97b7ffa..f2c82842bc1 100644 --- a/homeassistant/components/rfxtrx.py +++ b/homeassistant/components/rfxtrx.py @@ -4,7 +4,6 @@ Support for RFXtrx components. For more details about this component, please refer to the documentation at https://home-assistant.io/components/rfxtrx/ """ -import asyncio from collections import OrderedDict import logging @@ -316,8 +315,7 @@ class RfxtrxDevice(Entity): self._brightness = 0 self.added_to_hass = False - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Subscribe RFXtrx events.""" self.added_to_hass = True diff --git a/homeassistant/components/rss_feed_template.py b/homeassistant/components/rss_feed_template.py index 1441a98c0a8..34bee1ec5fc 100644 --- a/homeassistant/components/rss_feed_template.py +++ b/homeassistant/components/rss_feed_template.py @@ -5,7 +5,6 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/rss_feed_template/ """ -import asyncio from html import escape from aiohttp import web @@ -76,8 +75,7 @@ class RssView(HomeAssistantView): self._title = title self._items = items - @asyncio.coroutine - def get(self, request, entity_id=None): + async def get(self, request, entity_id=None): """Generate the RSS view XML.""" response = '\n\n' diff --git a/homeassistant/components/satel_integra.py b/homeassistant/components/satel_integra.py index 128377d19f7..8d7d1e619db 100644 --- a/homeassistant/components/satel_integra.py +++ b/homeassistant/components/satel_integra.py @@ -67,8 +67,7 @@ CONFIG_SCHEMA = vol.Schema({ }, extra=vol.ALLOW_EXTRA) -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Set up the Satel Integra component.""" conf = config.get(DOMAIN) @@ -83,13 +82,12 @@ def async_setup(hass, config): hass.data[DATA_SATEL] = controller - result = yield from controller.connect() + result = await controller.connect() if not result: return False - @asyncio.coroutine - def _close(): + async def _close(): controller.close() hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _close()) @@ -105,7 +103,7 @@ def async_setup(hass, config): async_load_platform(hass, 'binary_sensor', DOMAIN, {CONF_ZONES: zones}, config)) - yield from asyncio.wait([task_control_panel, task_zones], loop=hass.loop) + await asyncio.wait([task_control_panel, task_zones], loop=hass.loop) @callback def alarm_status_update_callback(status): diff --git a/homeassistant/components/shell_command.py b/homeassistant/components/shell_command.py index 10a6c350b7c..2a95dd5c144 100644 --- a/homeassistant/components/shell_command.py +++ b/homeassistant/components/shell_command.py @@ -27,15 +27,13 @@ CONFIG_SCHEMA = vol.Schema({ }, extra=vol.ALLOW_EXTRA) -@asyncio.coroutine -def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: +async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: """Set up the shell_command component.""" conf = config.get(DOMAIN, {}) cache = {} - @asyncio.coroutine - def async_service_handler(service: ServiceCall) -> None: + async def async_service_handler(service: ServiceCall) -> None: """Execute a shell command service.""" cmd = conf[service.service] @@ -85,8 +83,8 @@ def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: stderr=asyncio.subprocess.PIPE, ) - process = yield from create_process - stdout_data, stderr_data = yield from process.communicate() + process = await create_process + stdout_data, stderr_data = await process.communicate() if stdout_data: _LOGGER.debug("Stdout of command: `%s`, return code: %s:\n%s", diff --git a/homeassistant/components/sun.py b/homeassistant/components/sun.py index 90c7f69e64a..e2717047b0a 100644 --- a/homeassistant/components/sun.py +++ b/homeassistant/components/sun.py @@ -4,7 +4,6 @@ Support for functionality to keep track of the sun. For more details about this component, please refer to the documentation at https://home-assistant.io/components/sun/ """ -import asyncio import logging from datetime import timedelta @@ -36,8 +35,7 @@ STATE_ATTR_NEXT_RISING = 'next_rising' STATE_ATTR_NEXT_SETTING = 'next_setting' -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Track the state of the sun.""" if config.get(CONF_ELEVATION) is not None: _LOGGER.warning( diff --git a/homeassistant/components/system_log/__init__.py b/homeassistant/components/system_log/__init__.py index 2a2a19aa2f5..8ab6bd752ef 100644 --- a/homeassistant/components/system_log/__init__.py +++ b/homeassistant/components/system_log/__init__.py @@ -4,7 +4,6 @@ Support for system log. For more details about this component, please refer to the documentation at https://home-assistant.io/components/system_log/ """ -import asyncio from collections import deque from io import StringIO import logging @@ -134,8 +133,7 @@ class LogErrorHandler(logging.Handler): self.hass.bus.fire(EVENT_SYSTEM_LOG, entry) -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Set up the logger component.""" conf = config.get(DOMAIN) if conf is None: @@ -147,8 +145,7 @@ def async_setup(hass, config): hass.http.register_view(AllErrorsView(handler)) - @asyncio.coroutine - def async_service_handler(service): + async def async_service_handler(service): """Handle logger services.""" if service.service == 'clear': handler.records.clear() @@ -159,8 +156,7 @@ def async_setup(hass, config): level = service.data[CONF_LEVEL] getattr(logger, level)(service.data[CONF_MESSAGE]) - @asyncio.coroutine - def async_shutdown_handler(event): + async def async_shutdown_handler(event): """Remove logging handler when Home Assistant is shutdown.""" # This is needed as older logger instances will remain logging.getLogger().removeHandler(handler) @@ -188,8 +184,7 @@ class AllErrorsView(HomeAssistantView): """Initialize a new AllErrorsView.""" self.handler = handler - @asyncio.coroutine - def get(self, request): + async def get(self, request): """Get all errors and warnings.""" # deque is not serializable (it's just "list-like") so it must be # converted to a list before it can be serialized to json diff --git a/homeassistant/components/tellstick.py b/homeassistant/components/tellstick.py index 0eef2c4ece1..8f1c45d7312 100644 --- a/homeassistant/components/tellstick.py +++ b/homeassistant/components/tellstick.py @@ -4,7 +4,6 @@ Tellstick Component. For more details about this component, please refer to the documentation at https://home-assistant.io/components/tellstick/ """ -import asyncio import logging import threading @@ -158,8 +157,7 @@ class TellstickDevice(Entity): self._tellcore_device = tellcore_device self._name = tellcore_device.name - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Register callbacks.""" self.hass.helpers.dispatcher.async_dispatcher_connect( SIGNAL_TELLCORE_CALLBACK, diff --git a/homeassistant/components/thethingsnetwork.py b/homeassistant/components/thethingsnetwork.py index 08715c74d1f..61f9843be45 100644 --- a/homeassistant/components/thethingsnetwork.py +++ b/homeassistant/components/thethingsnetwork.py @@ -4,7 +4,6 @@ Support for The Things network. For more details about this component, please refer to the documentation at https://home-assistant.io/components/thethingsnetwork/ """ -import asyncio import logging import voluptuous as vol @@ -32,8 +31,7 @@ CONFIG_SCHEMA = vol.Schema({ }, extra=vol.ALLOW_EXTRA) -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Initialize of The Things Network component.""" conf = config[DOMAIN] app_id = conf.get(CONF_APP_ID) diff --git a/homeassistant/components/upcloud.py b/homeassistant/components/upcloud.py index 0f503dcdc39..a0b61f86e56 100644 --- a/homeassistant/components/upcloud.py +++ b/homeassistant/components/upcloud.py @@ -4,7 +4,6 @@ Support for UpCloud. For more details about this component, please refer to the documentation at https://home-assistant.io/components/upcloud/ """ -import asyncio import logging from datetime import timedelta @@ -129,8 +128,7 @@ class UpCloudServerEntity(Entity): except (AttributeError, KeyError, TypeError): return DEFAULT_COMPONENT_NAME.format(self.uuid) - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Register callbacks.""" async_dispatcher_connect( self.hass, SIGNAL_UPDATE_UPCLOUD, self._update_callback) diff --git a/homeassistant/components/wake_on_lan.py b/homeassistant/components/wake_on_lan.py index 5bcb0d4dd79..dba99bf7e3d 100644 --- a/homeassistant/components/wake_on_lan.py +++ b/homeassistant/components/wake_on_lan.py @@ -4,7 +4,6 @@ Component to wake up devices sending Wake-On-LAN magic packets. For more details about this component, please refer to the documentation at https://home-assistant.io/components/wake_on_lan/ """ -import asyncio from functools import partial import logging @@ -29,24 +28,22 @@ WAKE_ON_LAN_SEND_MAGIC_PACKET_SCHEMA = vol.Schema({ }) -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Set up the wake on LAN component.""" import wakeonlan - @asyncio.coroutine - def send_magic_packet(call): + async def send_magic_packet(call): """Send magic packet to wake up a device.""" mac_address = call.data.get(CONF_MAC) broadcast_address = call.data.get(CONF_BROADCAST_ADDRESS) _LOGGER.info("Send magic packet to mac %s (broadcast: %s)", mac_address, broadcast_address) if broadcast_address is not None: - yield from hass.async_add_job( + await hass.async_add_job( partial(wakeonlan.send_magic_packet, mac_address, ip_address=broadcast_address)) else: - yield from hass.async_add_job( + await hass.async_add_job( partial(wakeonlan.send_magic_packet, mac_address)) hass.services.async_register( diff --git a/homeassistant/components/weather/__init__.py b/homeassistant/components/weather/__init__.py index c2a9f4f79d1..f9a8f1fbbe4 100644 --- a/homeassistant/components/weather/__init__.py +++ b/homeassistant/components/weather/__init__.py @@ -4,7 +4,6 @@ Weather component that handles meteorological data for your location. For more details about this component, please refer to the documentation at https://home-assistant.io/components/weather/ """ -import asyncio import logging from homeassistant.helpers.entity_component import EntityComponent @@ -39,12 +38,11 @@ ATTR_WEATHER_WIND_BEARING = 'wind_bearing' ATTR_WEATHER_WIND_SPEED = 'wind_speed' -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Set up the weather component.""" component = EntityComponent(_LOGGER, DOMAIN, hass) - yield from component.async_setup(config) + await component.async_setup(config) return True diff --git a/homeassistant/components/weather/buienradar.py b/homeassistant/components/weather/buienradar.py index 6b92eb97c9e..1ec3fc513e9 100644 --- a/homeassistant/components/weather/buienradar.py +++ b/homeassistant/components/weather/buienradar.py @@ -5,7 +5,6 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/weather.buienradar/ """ import logging -import asyncio import voluptuous as vol @@ -55,9 +54,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the buienradar platform.""" latitude = config.get(CONF_LATITUDE, hass.config.latitude) longitude = config.get(CONF_LONGITUDE, hass.config.longitude) @@ -86,7 +84,7 @@ def async_setup_platform(hass, config, async_add_entities, async_add_entities([BrWeather(data, config)]) # schedule the first update in 1 minute from now: - yield from data.schedule_update(1) + await data.schedule_update(1) class BrWeather(WeatherEntity): diff --git a/homeassistant/components/wink/__init__.py b/homeassistant/components/wink/__init__.py index 0399b25b278..d21ccc18c93 100644 --- a/homeassistant/components/wink/__init__.py +++ b/homeassistant/components/wink/__init__.py @@ -4,7 +4,6 @@ Support for Wink hubs. For more details about this component, please refer to the documentation at https://home-assistant.io/components/wink/ """ -import asyncio from datetime import timedelta import json import logging @@ -763,8 +762,7 @@ class WinkDevice(Entity): class WinkSirenDevice(WinkDevice): """Representation of a Wink siren device.""" - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Call when entity is added to hass.""" self.hass.data[DOMAIN]['entities']['switch'].append(self) @@ -824,8 +822,7 @@ class WinkNimbusDialDevice(WinkDevice): super().__init__(dial, hass) self.parent = nimbus - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Call when entity is added to hass.""" self.hass.data[DOMAIN]['entities']['sensor'].append(self) diff --git a/homeassistant/components/xiaomi_aqara.py b/homeassistant/components/xiaomi_aqara.py index 9c2fb9f7fe7..790510db3d3 100644 --- a/homeassistant/components/xiaomi_aqara.py +++ b/homeassistant/components/xiaomi_aqara.py @@ -4,7 +4,6 @@ Support for Xiaomi Gateways. For more details about this component, please refer to the documentation at https://home-assistant.io/components/xiaomi_aqara/ """ -import asyncio import logging from datetime import timedelta @@ -113,8 +112,7 @@ def setup(hass, config): interface = config[DOMAIN][CONF_INTERFACE] discovery_retry = config[DOMAIN][CONF_DISCOVERY_RETRY] - @asyncio.coroutine - def xiaomi_gw_discovered(service, discovery_info): + async def xiaomi_gw_discovered(service, discovery_info): """Perform action when Xiaomi Gateway device(s) has been found.""" # We don't need to do anything here, the purpose of Home Assistant's # discovery service is to just trigger loading of this @@ -233,8 +231,7 @@ class XiaomiDevice(Entity): def _add_push_data_job(self, *args): self.hass.add_job(self.push_data, *args) - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Start unavailability tracking.""" self._xiaomi_hub.callbacks[self._sid].append(self._add_push_data_job) self._async_track_unavailable() diff --git a/homeassistant/components/zigbee.py b/homeassistant/components/zigbee.py index 67bdf744251..4c294e51231 100644 --- a/homeassistant/components/zigbee.py +++ b/homeassistant/components/zigbee.py @@ -4,7 +4,6 @@ Support for ZigBee devices. For more details about this component, please refer to the documentation at https://home-assistant.io/components/zigbee/ """ -import asyncio import logging from binascii import hexlify, unhexlify @@ -277,8 +276,7 @@ class ZigBeeDigitalIn(Entity): self._config = config self._state = False - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Register callbacks.""" def handle_frame(frame): """Handle an incoming frame. @@ -403,8 +401,7 @@ class ZigBeeAnalogIn(Entity): self._config = config self._value = None - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Register callbacks.""" def handle_frame(frame): """Handle an incoming frame. From 121dba659c0cf179e35de351f683be954edd091b Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Mon, 1 Oct 2018 08:55:00 +0200 Subject: [PATCH 120/247] Async syntax 7, switch & tts & vacuum (#17021) --- homeassistant/components/switch/ads.py | 4 +- homeassistant/components/switch/amcrest.py | 6 +- .../components/switch/android_ip_webcam.py | 31 +++----- homeassistant/components/switch/broadlink.py | 18 ++--- homeassistant/components/switch/hook.py | 30 +++---- homeassistant/components/switch/insteon.py | 18 ++--- .../components/switch/lutron_caseta.py | 15 ++-- homeassistant/components/switch/pilight.py | 6 +- homeassistant/components/switch/rest.py | 34 ++++---- homeassistant/components/switch/rflink.py | 6 +- homeassistant/components/switch/template.py | 22 ++---- homeassistant/components/switch/wink.py | 4 +- homeassistant/components/tts/marytts.py | 10 +-- homeassistant/components/tts/voicerss.py | 10 +-- homeassistant/components/tts/yandextts.py | 10 +-- homeassistant/components/vacuum/__init__.py | 6 +- homeassistant/components/vacuum/dyson.py | 4 +- homeassistant/components/vacuum/mqtt.py | 40 ++++------ homeassistant/components/vacuum/roomba.py | 63 ++++++--------- .../components/vacuum/xiaomi_miio.py | 79 ++++++++----------- 20 files changed, 163 insertions(+), 253 deletions(-) diff --git a/homeassistant/components/switch/ads.py b/homeassistant/components/switch/ads.py index 8c13e9a8960..ecd1e7edc31 100644 --- a/homeassistant/components/switch/ads.py +++ b/homeassistant/components/switch/ads.py @@ -4,7 +4,6 @@ Support for ADS switch platform. For more details about this platform, please refer to the documentation. https://home-assistant.io/components/switch.ads/ """ -import asyncio import logging import voluptuous as vol @@ -47,8 +46,7 @@ class AdsSwitch(ToggleEntity): self._name = name self.ads_var = ads_var - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Register device notification.""" def update(name, value): """Handle device notification.""" diff --git a/homeassistant/components/switch/amcrest.py b/homeassistant/components/switch/amcrest.py index 0805793fe95..4eb20308850 100644 --- a/homeassistant/components/switch/amcrest.py +++ b/homeassistant/components/switch/amcrest.py @@ -4,7 +4,6 @@ Support for toggling Amcrest IP camera settings. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/switch.amcrest/ """ -import asyncio import logging from homeassistant.components.amcrest import DATA_AMCREST, SWITCHES @@ -17,9 +16,8 @@ _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['amcrest'] -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the IP Amcrest camera switch platform.""" if discovery_info is None: return diff --git a/homeassistant/components/switch/android_ip_webcam.py b/homeassistant/components/switch/android_ip_webcam.py index 92e52c21caa..f770b9d5ebf 100644 --- a/homeassistant/components/switch/android_ip_webcam.py +++ b/homeassistant/components/switch/android_ip_webcam.py @@ -4,7 +4,6 @@ Support for IP Webcam settings. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/switch.android_ip_webcam/ """ -import asyncio from homeassistant.components.switch import SwitchDevice from homeassistant.components.android_ip_webcam import ( @@ -14,9 +13,8 @@ from homeassistant.components.android_ip_webcam import ( DEPENDENCIES = ['android_ip_webcam'] -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the IP Webcam switch platform.""" if discovery_info is None: return @@ -51,8 +49,7 @@ class IPWebcamSettingsSwitch(AndroidIPCamEntity, SwitchDevice): """Return the name of the node.""" return self._name - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Get the updated status of the switch.""" self._state = bool(self._ipcam.current_settings.get(self._setting)) @@ -61,31 +58,29 @@ class IPWebcamSettingsSwitch(AndroidIPCamEntity, SwitchDevice): """Return the boolean response if the node is on.""" return self._state - @asyncio.coroutine - def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): """Turn device on.""" if self._setting == 'torch': - yield from self._ipcam.torch(activate=True) + await self._ipcam.torch(activate=True) elif self._setting == 'focus': - yield from self._ipcam.focus(activate=True) + await self._ipcam.focus(activate=True) elif self._setting == 'video_recording': - yield from self._ipcam.record(record=True) + await self._ipcam.record(record=True) else: - yield from self._ipcam.change_setting(self._setting, True) + await self._ipcam.change_setting(self._setting, True) self._state = True self.async_schedule_update_ha_state() - @asyncio.coroutine - def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs): """Turn device off.""" if self._setting == 'torch': - yield from self._ipcam.torch(activate=False) + await self._ipcam.torch(activate=False) elif self._setting == 'focus': - yield from self._ipcam.focus(activate=False) + await self._ipcam.focus(activate=False) elif self._setting == 'video_recording': - yield from self._ipcam.record(record=False) + await self._ipcam.record(record=False) else: - yield from self._ipcam.change_setting(self._setting, False) + await self._ipcam.change_setting(self._setting, False) self._state = False self.async_schedule_update_ha_state() diff --git a/homeassistant/components/switch/broadlink.py b/homeassistant/components/switch/broadlink.py index e6115872390..0562292acec 100644 --- a/homeassistant/components/switch/broadlink.py +++ b/homeassistant/components/switch/broadlink.py @@ -80,11 +80,10 @@ def setup_platform(hass, config, add_entities, discovery_info=None): config.get(CONF_MAC).encode().replace(b':', b'')) switch_type = config.get(CONF_TYPE) - @asyncio.coroutine - def _learn_command(call): + async def _learn_command(call): """Handle a learn command.""" try: - auth = yield from hass.async_add_job(broadlink_device.auth) + auth = await hass.async_add_job(broadlink_device.auth) except socket.timeout: _LOGGER.error("Failed to connect to device, timeout") return @@ -92,12 +91,12 @@ def setup_platform(hass, config, add_entities, discovery_info=None): _LOGGER.error("Failed to connect to device") return - yield from hass.async_add_job(broadlink_device.enter_learning) + await hass.async_add_job(broadlink_device.enter_learning) _LOGGER.info("Press the key you want Home Assistant to learn") start_time = utcnow() while (utcnow() - start_time) < timedelta(seconds=20): - packet = yield from hass.async_add_job( + packet = await hass.async_add_job( broadlink_device.check_data) if packet: log_msg = "Received packet is: {}".\ @@ -106,13 +105,12 @@ def setup_platform(hass, config, add_entities, discovery_info=None): hass.components.persistent_notification.async_create( log_msg, title='Broadlink switch') return - yield from asyncio.sleep(1, loop=hass.loop) + await asyncio.sleep(1, loop=hass.loop) _LOGGER.error("Did not received any signal") hass.components.persistent_notification.async_create( "Did not received any signal", title='Broadlink switch') - @asyncio.coroutine - def _send_packet(call): + async def _send_packet(call): """Send a packet.""" packets = call.data.get('packet', []) for packet in packets: @@ -122,12 +120,12 @@ def setup_platform(hass, config, add_entities, discovery_info=None): if extra > 0: packet = packet + ('=' * (4 - extra)) payload = b64decode(packet) - yield from hass.async_add_job( + await hass.async_add_job( broadlink_device.send_data, payload) break except (socket.timeout, ValueError): try: - yield from hass.async_add_job( + await hass.async_add_job( broadlink_device.auth) except socket.timeout: if retry == DEFAULT_RETRY-1: diff --git a/homeassistant/components/switch/hook.py b/homeassistant/components/switch/hook.py index 5a86346aa76..fdc13f59d28 100644 --- a/homeassistant/components/switch/hook.py +++ b/homeassistant/components/switch/hook.py @@ -31,9 +31,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up Hook by getting the access token and list of actions.""" username = config.get(CONF_USERNAME) password = config.get(CONF_PASSWORD) @@ -43,14 +42,14 @@ def async_setup_platform(hass, config, async_add_entities, if username is not None and password is not None: try: with async_timeout.timeout(TIMEOUT, loop=hass.loop): - response = yield from websession.post( + response = await websession.post( '{}{}'.format(HOOK_ENDPOINT, 'user/login'), data={ 'username': username, 'password': password}) # The Hook API returns JSON but calls it 'text/html'. Setting # content_type=None disables aiohttp's content-type validation. - data = yield from response.json(content_type=None) + data = await response.json(content_type=None) except (asyncio.TimeoutError, aiohttp.ClientError) as error: _LOGGER.error("Failed authentication API call: %s", error) return False @@ -63,10 +62,10 @@ def async_setup_platform(hass, config, async_add_entities, try: with async_timeout.timeout(TIMEOUT, loop=hass.loop): - response = yield from websession.get( + response = await websession.get( '{}{}'.format(HOOK_ENDPOINT, 'device'), params={"token": token}) - data = yield from response.json(content_type=None) + data = await response.json(content_type=None) except (asyncio.TimeoutError, aiohttp.ClientError) as error: _LOGGER.error("Failed getting devices: %s", error) return False @@ -104,16 +103,15 @@ class HookSmartHome(SwitchDevice): """Return true if device is on.""" return self._state - @asyncio.coroutine - def _send(self, url): + async def _send(self, url): """Send the url to the Hook API.""" try: _LOGGER.debug("Sending: %s", url) websession = async_get_clientsession(self.hass) with async_timeout.timeout(TIMEOUT, loop=self.hass.loop): - response = yield from websession.get( + response = await websession.get( url, params={"token": self._token}) - data = yield from response.json(content_type=None) + data = await response.json(content_type=None) except (asyncio.TimeoutError, aiohttp.ClientError) as error: _LOGGER.error("Failed setting state: %s", error) @@ -122,21 +120,19 @@ class HookSmartHome(SwitchDevice): _LOGGER.debug("Got: %s", data) return data['return_value'] == '1' - @asyncio.coroutine - def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): """Turn the device on asynchronously.""" _LOGGER.debug("Turning on: %s", self._name) url = '{}{}{}{}'.format( HOOK_ENDPOINT, 'device/trigger/', self._id, '/On') - success = yield from self._send(url) + success = await self._send(url) self._state = success - @asyncio.coroutine - def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs): """Turn the device off asynchronously.""" _LOGGER.debug("Turning off: %s", self._name) url = '{}{}{}{}'.format( HOOK_ENDPOINT, 'device/trigger/', self._id, '/Off') - success = yield from self._send(url) + success = await self._send(url) # If it wasn't successful, keep state as true self._state = not success diff --git a/homeassistant/components/switch/insteon.py b/homeassistant/components/switch/insteon.py index 744d278d394..454b3ef39cb 100644 --- a/homeassistant/components/switch/insteon.py +++ b/homeassistant/components/switch/insteon.py @@ -4,7 +4,6 @@ Support for INSTEON dimmers via PowerLinc Modem. For more details about this component, please refer to the documentation at https://home-assistant.io/components/switch.insteon/ """ -import asyncio import logging from homeassistant.components.insteon import InsteonEntity @@ -15,9 +14,8 @@ DEPENDENCIES = ['insteon'] _LOGGER = logging.getLogger(__name__) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the INSTEON device class for the hass platform.""" insteon_modem = hass.data['insteon'].get('modem') @@ -48,13 +46,11 @@ class InsteonSwitchDevice(InsteonEntity, SwitchDevice): """Return the boolean response if the node is on.""" return bool(self._insteon_device_state.value) - @asyncio.coroutine - def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): """Turn device on.""" self._insteon_device_state.on() - @asyncio.coroutine - def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs): """Turn device off.""" self._insteon_device_state.off() @@ -67,12 +63,10 @@ class InsteonOpenClosedDevice(InsteonEntity, SwitchDevice): """Return the boolean response if the node is on.""" return bool(self._insteon_device_state.value) - @asyncio.coroutine - def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): """Turn device on.""" self._insteon_device_state.open() - @asyncio.coroutine - def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs): """Turn device off.""" self._insteon_device_state.close() diff --git a/homeassistant/components/switch/lutron_caseta.py b/homeassistant/components/switch/lutron_caseta.py index 8587c78a5d5..f983050cffa 100644 --- a/homeassistant/components/switch/lutron_caseta.py +++ b/homeassistant/components/switch/lutron_caseta.py @@ -4,7 +4,6 @@ Support for Lutron Caseta switches. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sitch.lutron_caseta/ """ -import asyncio import logging from homeassistant.components.lutron_caseta import ( @@ -16,9 +15,8 @@ _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['lutron_caseta'] -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up Lutron switch.""" devs = [] bridge = hass.data[LUTRON_CASETA_SMARTBRIDGE] @@ -35,13 +33,11 @@ def async_setup_platform(hass, config, async_add_entities, class LutronCasetaLight(LutronCasetaDevice, SwitchDevice): """Representation of a Lutron Caseta switch.""" - @asyncio.coroutine - def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): """Turn the switch on.""" self._smartbridge.turn_on(self._device_id) - @asyncio.coroutine - def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs): """Turn the switch off.""" self._smartbridge.turn_off(self._device_id) @@ -50,8 +46,7 @@ class LutronCasetaLight(LutronCasetaDevice, SwitchDevice): """Return true if device is on.""" return self._state["current_state"] > 0 - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Update when forcing a refresh of the device.""" self._state = self._smartbridge.get_device_by_id(self._device_id) _LOGGER.debug(self._state) diff --git a/homeassistant/components/switch/pilight.py b/homeassistant/components/switch/pilight.py index 8ae8e64c2ff..16dfc075409 100644 --- a/homeassistant/components/switch/pilight.py +++ b/homeassistant/components/switch/pilight.py @@ -4,7 +4,6 @@ Support for switching devices via Pilight to on and off. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/switch.pilight/ """ -import asyncio import logging import voluptuous as vol @@ -122,10 +121,9 @@ class PilightSwitch(SwitchDevice): if any(self._code_on_receive) or any(self._code_off_receive): hass.bus.listen(pilight.EVENT, self._handle_code) - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Call when entity about to be added to hass.""" - state = yield from async_get_last_state(self._hass, self.entity_id) + state = await async_get_last_state(self._hass, self.entity_id) if state: self._state = state.state == STATE_ON diff --git a/homeassistant/components/switch/rest.py b/homeassistant/components/switch/rest.py index 0e00bfe7844..9dac5130593 100644 --- a/homeassistant/components/switch/rest.py +++ b/homeassistant/components/switch/rest.py @@ -47,9 +47,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the RESTful switch.""" body_off = config.get(CONF_BODY_OFF) body_on = config.get(CONF_BODY_ON) @@ -77,7 +76,7 @@ def async_setup_platform(hass, config, async_add_entities, switch = RestSwitch(name, resource, method, headers, auth, body_on, body_off, is_on_template, timeout) - req = yield from switch.get_device_state(hass) + req = await switch.get_device_state(hass) if req.status >= 400: _LOGGER.error("Got non-ok response from resource: %s", req.status) else: @@ -116,13 +115,12 @@ class RestSwitch(SwitchDevice): """Return true if device is on.""" return self._state - @asyncio.coroutine - def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): """Turn the device on.""" body_on_t = self._body_on.async_render() try: - req = yield from self.set_device_state(body_on_t) + req = await self.set_device_state(body_on_t) if req.status == 200: self._state = True @@ -133,13 +131,12 @@ class RestSwitch(SwitchDevice): except (asyncio.TimeoutError, aiohttp.ClientError): _LOGGER.error("Error while turn on %s", self._resource) - @asyncio.coroutine - def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs): """Turn the device off.""" body_off_t = self._body_off.async_render() try: - req = yield from self.set_device_state(body_off_t) + req = await self.set_device_state(body_off_t) if req.status == 200: self._state = False else: @@ -149,33 +146,30 @@ class RestSwitch(SwitchDevice): except (asyncio.TimeoutError, aiohttp.ClientError): _LOGGER.error("Error while turn off %s", self._resource) - @asyncio.coroutine - def set_device_state(self, body): + async def set_device_state(self, body): """Send a state update to the device.""" websession = async_get_clientsession(self.hass) with async_timeout.timeout(self._timeout, loop=self.hass.loop): - req = yield from getattr(websession, self._method)( + req = await getattr(websession, self._method)( self._resource, auth=self._auth, data=bytes(body, 'utf-8'), headers=self._headers) return req - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Get the current state, catching errors.""" try: - yield from self.get_device_state(self.hass) + await self.get_device_state(self.hass) except (asyncio.TimeoutError, aiohttp.ClientError): _LOGGER.exception("Error while fetch data.") - @asyncio.coroutine - def get_device_state(self, hass): + async def get_device_state(self, hass): """Get the latest data from REST API and update the state.""" websession = async_get_clientsession(hass) with async_timeout.timeout(self._timeout, loop=hass.loop): - req = yield from websession.get(self._resource, auth=self._auth) - text = yield from req.text() + req = await websession.get(self._resource, auth=self._auth) + text = await req.text() if self._is_on_template is not None: text = self._is_on_template.async_render_with_possible_json_value( diff --git a/homeassistant/components/switch/rflink.py b/homeassistant/components/switch/rflink.py index 370436b3184..2bbe3e3f03d 100644 --- a/homeassistant/components/switch/rflink.py +++ b/homeassistant/components/switch/rflink.py @@ -4,7 +4,6 @@ Support for Rflink switches. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/switch.rflink/ """ -import asyncio import logging from homeassistant.components.rflink import ( @@ -85,9 +84,8 @@ def devices_from_config(domain_config, hass=None): return devices -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the Rflink platform.""" async_add_entities(devices_from_config(config, hass)) diff --git a/homeassistant/components/switch/template.py b/homeassistant/components/switch/template.py index 7461aa2a720..724fcbf6075 100644 --- a/homeassistant/components/switch/template.py +++ b/homeassistant/components/switch/template.py @@ -4,7 +4,6 @@ Support for switches which integrates with other components. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/switch.template/ """ -import asyncio import logging import voluptuous as vol @@ -43,9 +42,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the Template switch.""" switches = [] @@ -103,8 +101,7 @@ class SwitchTemplate(SwitchDevice): self._entity_picture = None self._entities = entity_ids - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Register callbacks.""" @callback def template_switch_state_listener(entity, old_state, new_state): @@ -152,18 +149,15 @@ class SwitchTemplate(SwitchDevice): """Return the entity_picture to use in the frontend, if any.""" return self._entity_picture - @asyncio.coroutine - def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): """Fire the on action.""" - yield from self._on_script.async_run() + await self._on_script.async_run() - @asyncio.coroutine - def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs): """Fire the off action.""" - yield from self._off_script.async_run() + await self._off_script.async_run() - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Update the state from the template.""" try: state = self._template.async_render().lower() diff --git a/homeassistant/components/switch/wink.py b/homeassistant/components/switch/wink.py index 0df59d6b51c..9dea93488af 100644 --- a/homeassistant/components/switch/wink.py +++ b/homeassistant/components/switch/wink.py @@ -4,7 +4,6 @@ Support for Wink switches. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/switch.wink/ """ -import asyncio import logging from homeassistant.components.wink import DOMAIN, WinkDevice @@ -40,8 +39,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class WinkToggleDevice(WinkDevice, ToggleEntity): """Representation of a Wink toggle device.""" - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Call when entity is added to hass.""" self.hass.data[DOMAIN]['entities']['switch'].append(self) diff --git a/homeassistant/components/tts/marytts.py b/homeassistant/components/tts/marytts.py index 072ea0e76e7..61f01a9b292 100644 --- a/homeassistant/components/tts/marytts.py +++ b/homeassistant/components/tts/marytts.py @@ -45,8 +45,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_get_engine(hass, config): +async def async_get_engine(hass, config): """Set up MaryTTS speech component.""" return MaryTTSProvider(hass, config) @@ -74,8 +73,7 @@ class MaryTTSProvider(Provider): """Return list of supported languages.""" return SUPPORT_LANGUAGES - @asyncio.coroutine - def async_get_tts_audio(self, message, language, options=None): + async def async_get_tts_audio(self, message, language, options=None): """Load TTS from MaryTTS.""" websession = async_get_clientsession(self.hass) @@ -98,13 +96,13 @@ class MaryTTSProvider(Provider): 'LOCALE': actual_language } - request = yield from websession.get(url, params=url_param) + request = await websession.get(url, params=url_param) if request.status != 200: _LOGGER.error("Error %d on load url %s", request.status, request.url) return (None, None) - data = yield from request.read() + data = await request.read() except (asyncio.TimeoutError, aiohttp.ClientError): _LOGGER.error("Timeout for MaryTTS API") diff --git a/homeassistant/components/tts/voicerss.py b/homeassistant/components/tts/voicerss.py index 38f6e2290b5..22eba69e510 100644 --- a/homeassistant/components/tts/voicerss.py +++ b/homeassistant/components/tts/voicerss.py @@ -80,8 +80,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_get_engine(hass, config): +async def async_get_engine(hass, config): """Set up VoiceRSS TTS component.""" return VoiceRSSProvider(hass, config) @@ -113,8 +112,7 @@ class VoiceRSSProvider(Provider): """Return list of supported languages.""" return SUPPORT_LANGUAGES - @asyncio.coroutine - def async_get_tts_audio(self, message, language, options=None): + async def async_get_tts_audio(self, message, language, options=None): """Load TTS from VoiceRSS.""" websession = async_get_clientsession(self.hass) form_data = self._form_data.copy() @@ -124,7 +122,7 @@ class VoiceRSSProvider(Provider): try: with async_timeout.timeout(10, loop=self.hass.loop): - request = yield from websession.post( + request = await websession.post( VOICERSS_API_URL, data=form_data ) @@ -132,7 +130,7 @@ class VoiceRSSProvider(Provider): _LOGGER.error("Error %d on load url %s.", request.status, request.url) return (None, None) - data = yield from request.read() + data = await request.read() if data in ERROR_MSG: _LOGGER.error( diff --git a/homeassistant/components/tts/yandextts.py b/homeassistant/components/tts/yandextts.py index b5e965a5b50..d0ec8c74a96 100644 --- a/homeassistant/components/tts/yandextts.py +++ b/homeassistant/components/tts/yandextts.py @@ -71,8 +71,7 @@ SUPPORTED_OPTIONS = [ ] -@asyncio.coroutine -def async_get_engine(hass, config): +async def async_get_engine(hass, config): """Set up VoiceRSS speech component.""" return YandexSpeechKitProvider(hass, config) @@ -106,8 +105,7 @@ class YandexSpeechKitProvider(Provider): """Return list of supported options.""" return SUPPORTED_OPTIONS - @asyncio.coroutine - def async_get_tts_audio(self, message, language, options=None): + async def async_get_tts_audio(self, message, language, options=None): """Load TTS from yandex.""" websession = async_get_clientsession(self.hass) actual_language = language @@ -125,14 +123,14 @@ class YandexSpeechKitProvider(Provider): 'speed': options.get(CONF_SPEED, self._speed) } - request = yield from websession.get( + request = await websession.get( YANDEX_API_URL, params=url_param) if request.status != 200: _LOGGER.error("Error %d on load URL %s", request.status, request.url) return (None, None) - data = yield from request.read() + data = await request.read() except (asyncio.TimeoutError, aiohttp.ClientError): _LOGGER.error("Timeout for yandex speech kit API") diff --git a/homeassistant/components/vacuum/__init__.py b/homeassistant/components/vacuum/__init__.py index 7a1c3049152..212e6bd648f 100644 --- a/homeassistant/components/vacuum/__init__.py +++ b/homeassistant/components/vacuum/__init__.py @@ -4,7 +4,6 @@ Support for vacuum cleaner robots (botvacs). For more details about this platform, please refer to the documentation https://home-assistant.io/components/vacuum/ """ -import asyncio from datetime import timedelta from functools import partial import logging @@ -94,13 +93,12 @@ def is_on(hass, entity_id=None): return hass.states.is_state(entity_id, STATE_ON) -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Set up the vacuum component.""" component = EntityComponent( _LOGGER, DOMAIN, hass, SCAN_INTERVAL, GROUP_NAME_ALL_VACUUMS) - yield from component.async_setup(config) + await component.async_setup(config) component.async_register_entity_service( SERVICE_TURN_ON, VACUUM_SERVICE_SCHEMA, diff --git a/homeassistant/components/vacuum/dyson.py b/homeassistant/components/vacuum/dyson.py index 943b97f6360..3d6e23c20c8 100644 --- a/homeassistant/components/vacuum/dyson.py +++ b/homeassistant/components/vacuum/dyson.py @@ -4,7 +4,6 @@ Support for the Dyson 360 eye vacuum cleaner robot. For more details about this platform, please refer to the documentation https://home-assistant.io/components/vacuum.dyson/ """ -import asyncio import logging from homeassistant.components.dyson import DYSON_DEVICES @@ -55,8 +54,7 @@ class Dyson360EyeDevice(VacuumDevice): _LOGGER.debug("Creating device %s", device.name) self._device = device - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Call when entity is added to hass.""" self.hass.async_add_job( self._device.add_message_listener, self.on_message) diff --git a/homeassistant/components/vacuum/mqtt.py b/homeassistant/components/vacuum/mqtt.py index 47617277773..fcb77e10732 100644 --- a/homeassistant/components/vacuum/mqtt.py +++ b/homeassistant/components/vacuum/mqtt.py @@ -4,7 +4,6 @@ Support for a generic MQTT vacuum. For more details about this platform, please refer to the documentation https://home-assistant.io/components/vacuum.mqtt/ """ -import asyncio import logging import voluptuous as vol @@ -139,9 +138,8 @@ PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({ }).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +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) @@ -265,10 +263,9 @@ class MqttVacuum(MqttAvailability, VacuumDevice): self._battery_level = 0 self._fan_speed = 'unknown' - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Subscribe MQTT events.""" - yield from super().async_added_to_hass() + await super().async_added_to_hass() @callback def message_received(topic, payload, qos): @@ -332,7 +329,7 @@ class MqttVacuum(MqttAvailability, VacuumDevice): self._docked_topic, self._fan_speed_topic) if topic] for topic in set(topics_list): - yield from self.hass.components.mqtt.async_subscribe( + await self.hass.components.mqtt.async_subscribe( topic, message_received, self._qos) @property @@ -395,8 +392,7 @@ class MqttVacuum(MqttAvailability, VacuumDevice): """Flag supported features.""" return self._supported_features - @asyncio.coroutine - def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): """Turn the vacuum on.""" if self.supported_features & SUPPORT_TURN_ON == 0: return @@ -406,8 +402,7 @@ class MqttVacuum(MqttAvailability, VacuumDevice): self._status = 'Cleaning' self.async_schedule_update_ha_state() - @asyncio.coroutine - def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs): """Turn the vacuum off.""" if self.supported_features & SUPPORT_TURN_OFF == 0: return @@ -417,8 +412,7 @@ class MqttVacuum(MqttAvailability, VacuumDevice): self._status = 'Turning Off' self.async_schedule_update_ha_state() - @asyncio.coroutine - def async_stop(self, **kwargs): + async def async_stop(self, **kwargs): """Stop the vacuum.""" if self.supported_features & SUPPORT_STOP == 0: return @@ -428,8 +422,7 @@ class MqttVacuum(MqttAvailability, VacuumDevice): self._status = 'Stopping the current task' self.async_schedule_update_ha_state() - @asyncio.coroutine - def async_clean_spot(self, **kwargs): + async def async_clean_spot(self, **kwargs): """Perform a spot clean-up.""" if self.supported_features & SUPPORT_CLEAN_SPOT == 0: return @@ -439,8 +432,7 @@ class MqttVacuum(MqttAvailability, VacuumDevice): self._status = "Cleaning spot" self.async_schedule_update_ha_state() - @asyncio.coroutine - def async_locate(self, **kwargs): + async def async_locate(self, **kwargs): """Locate the vacuum (usually by playing a song).""" if self.supported_features & SUPPORT_LOCATE == 0: return @@ -450,8 +442,7 @@ class MqttVacuum(MqttAvailability, VacuumDevice): self._status = "Hi, I'm over here!" self.async_schedule_update_ha_state() - @asyncio.coroutine - def async_start_pause(self, **kwargs): + async def async_start_pause(self, **kwargs): """Start, pause or resume the cleaning task.""" if self.supported_features & SUPPORT_PAUSE == 0: return @@ -461,8 +452,7 @@ class MqttVacuum(MqttAvailability, VacuumDevice): self._status = 'Pausing/Resuming cleaning...' self.async_schedule_update_ha_state() - @asyncio.coroutine - def async_return_to_base(self, **kwargs): + async def async_return_to_base(self, **kwargs): """Tell the vacuum to return to its dock.""" if self.supported_features & SUPPORT_RETURN_HOME == 0: return @@ -473,8 +463,7 @@ class MqttVacuum(MqttAvailability, VacuumDevice): self._status = 'Returning home...' self.async_schedule_update_ha_state() - @asyncio.coroutine - def async_set_fan_speed(self, fan_speed, **kwargs): + async def async_set_fan_speed(self, fan_speed, **kwargs): """Set fan speed.""" if self.supported_features & SUPPORT_FAN_SPEED == 0: return @@ -487,8 +476,7 @@ class MqttVacuum(MqttAvailability, VacuumDevice): self._status = "Setting fan to {}...".format(fan_speed) self.async_schedule_update_ha_state() - @asyncio.coroutine - def async_send_command(self, command, params=None, **kwargs): + async def async_send_command(self, command, params=None, **kwargs): """Send a command to a vacuum cleaner.""" if self.supported_features & SUPPORT_SEND_COMMAND == 0: return diff --git a/homeassistant/components/vacuum/roomba.py b/homeassistant/components/vacuum/roomba.py index 487fd573f37..72d564909a8 100644 --- a/homeassistant/components/vacuum/roomba.py +++ b/homeassistant/components/vacuum/roomba.py @@ -68,9 +68,8 @@ SUPPORT_ROOMBA = SUPPORT_BATTERY | SUPPORT_PAUSE | SUPPORT_RETURN_HOME | \ SUPPORT_ROOMBA_CARPET_BOOST = SUPPORT_ROOMBA | SUPPORT_FAN_SPEED -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the iRobot Roomba vacuum cleaner platform.""" from roomba import Roomba if PLATFORM not in hass.data: @@ -96,7 +95,7 @@ def async_setup_platform(hass, config, async_add_entities, try: with async_timeout.timeout(9): - yield from hass.async_add_job(roomba.connect) + await hass.async_add_job(roomba.connect) except asyncio.TimeoutError: raise PlatformNotReady @@ -170,54 +169,46 @@ class RoombaVacuum(VacuumDevice): """Return the state attributes of the device.""" return self._state_attrs - @asyncio.coroutine - def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): """Turn the vacuum on.""" - yield from self.hass.async_add_job(self.vacuum.send_command, 'start') + await self.hass.async_add_job(self.vacuum.send_command, 'start') self._is_on = True - @asyncio.coroutine - def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs): """Turn the vacuum off and return to home.""" - yield from self.async_stop() - yield from self.async_return_to_base() + await self.async_stop() + await self.async_return_to_base() - @asyncio.coroutine - def async_stop(self, **kwargs): + async def async_stop(self, **kwargs): """Stop the vacuum cleaner.""" - yield from self.hass.async_add_job(self.vacuum.send_command, 'stop') + await self.hass.async_add_job(self.vacuum.send_command, 'stop') self._is_on = False - @asyncio.coroutine - def async_resume(self, **kwargs): + async def async_resume(self, **kwargs): """Resume the cleaning cycle.""" - yield from self.hass.async_add_job(self.vacuum.send_command, 'resume') + await self.hass.async_add_job(self.vacuum.send_command, 'resume') self._is_on = True - @asyncio.coroutine - def async_pause(self, **kwargs): + async def async_pause(self, **kwargs): """Pause the cleaning cycle.""" - yield from self.hass.async_add_job(self.vacuum.send_command, 'pause') + await self.hass.async_add_job(self.vacuum.send_command, 'pause') self._is_on = False - @asyncio.coroutine - def async_start_pause(self, **kwargs): + async def async_start_pause(self, **kwargs): """Pause the cleaning task or resume it.""" if self.vacuum_state and self.is_on: # vacuum is running - yield from self.async_pause() + await self.async_pause() elif self._status == 'Stopped': # vacuum is stopped - yield from self.async_resume() + await self.async_resume() else: # vacuum is off - yield from self.async_turn_on() + await self.async_turn_on() - @asyncio.coroutine - def async_return_to_base(self, **kwargs): + async def async_return_to_base(self, **kwargs): """Set the vacuum cleaner to return to the dock.""" - yield from self.hass.async_add_job(self.vacuum.send_command, 'dock') + await self.hass.async_add_job(self.vacuum.send_command, 'dock') self._is_on = False - @asyncio.coroutine - def async_set_fan_speed(self, fan_speed, **kwargs): + async def async_set_fan_speed(self, fan_speed, **kwargs): """Set fan speed.""" if fan_speed.capitalize() in FAN_SPEEDS: fan_speed = fan_speed.capitalize() @@ -240,22 +231,20 @@ class RoombaVacuum(VacuumDevice): _LOGGER.error("No such fan speed available: %s", fan_speed) return # The set_preference method does only accept string values - yield from self.hass.async_add_job( + await self.hass.async_add_job( self.vacuum.set_preference, 'carpetBoost', str(carpet_boost)) - yield from self.hass.async_add_job( + await self.hass.async_add_job( self.vacuum.set_preference, 'vacHigh', str(high_perf)) - @asyncio.coroutine - def async_send_command(self, command, params=None, **kwargs): + async def async_send_command(self, command, params=None, **kwargs): """Send raw command.""" _LOGGER.debug("async_send_command %s (%s), %s", command, params, kwargs) - yield from self.hass.async_add_job( + await self.hass.async_add_job( self.vacuum.send_command, command, params) return True - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Fetch state from the device.""" # No data, no update if not self.vacuum.master_state: diff --git a/homeassistant/components/vacuum/xiaomi_miio.py b/homeassistant/components/vacuum/xiaomi_miio.py index 290c3417149..78b3b4434d6 100644 --- a/homeassistant/components/vacuum/xiaomi_miio.py +++ b/homeassistant/components/vacuum/xiaomi_miio.py @@ -103,9 +103,8 @@ STATE_CODE_TO_STATE = { } -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the Xiaomi vacuum cleaner robot platform.""" from miio import Vacuum if DATA_KEY not in hass.data: @@ -124,8 +123,7 @@ def async_setup_platform(hass, config, async_add_entities, async_add_entities([mirobo], update_before_add=True) - @asyncio.coroutine - def async_service_handler(service): + async def async_service_handler(service): """Map services to methods on MiroboVacuum.""" method = SERVICE_TO_METHOD.get(service.service) params = {key: value for key, value in service.data.items() @@ -139,14 +137,14 @@ def async_setup_platform(hass, config, async_add_entities, update_tasks = [] for vacuum in target_vacuums: - yield from getattr(vacuum, method['method'])(**params) + await getattr(vacuum, method['method'])(**params) for vacuum in target_vacuums: update_coro = vacuum.async_update_ha_state(True) update_tasks.append(update_coro) if update_tasks: - yield from asyncio.wait(update_tasks, loop=hass.loop) + await asyncio.wait(update_tasks, loop=hass.loop) for vacuum_service in SERVICE_TO_METHOD: schema = SERVICE_TO_METHOD[vacuum_service].get( @@ -259,12 +257,11 @@ class MiroboVacuum(StateVacuumDevice): """Flag vacuum cleaner robot features that are supported.""" return SUPPORT_XIAOMI - @asyncio.coroutine - def _try_command(self, mask_error, func, *args, **kwargs): + async def _try_command(self, mask_error, func, *args, **kwargs): """Call a vacuum command handling error messages.""" from miio import DeviceException try: - yield from self.hass.async_add_job(partial(func, *args, **kwargs)) + await self.hass.async_add_job(partial(func, *args, **kwargs)) return True except DeviceException as exc: _LOGGER.error(mask_error, exc) @@ -281,14 +278,12 @@ class MiroboVacuum(StateVacuumDevice): await self._try_command( "Unable to set start/pause: %s", self._vacuum.pause) - @asyncio.coroutine - def async_stop(self, **kwargs): + async def async_stop(self, **kwargs): """Stop the vacuum cleaner.""" - yield from self._try_command( + await self._try_command( "Unable to stop: %s", self._vacuum.stop) - @asyncio.coroutine - def async_set_fan_speed(self, fan_speed, **kwargs): + async def async_set_fan_speed(self, fan_speed, **kwargs): """Set fan speed.""" if fan_speed.capitalize() in FAN_SPEEDS: fan_speed = FAN_SPEEDS[fan_speed.capitalize()] @@ -300,68 +295,60 @@ class MiroboVacuum(StateVacuumDevice): "Valid speeds are: %s", exc, self.fan_speed_list) return - yield from self._try_command( + await self._try_command( "Unable to set fan speed: %s", self._vacuum.set_fan_speed, fan_speed) - @asyncio.coroutine - def async_return_to_base(self, **kwargs): + async def async_return_to_base(self, **kwargs): """Set the vacuum cleaner to return to the dock.""" - yield from self._try_command( + await self._try_command( "Unable to return home: %s", self._vacuum.home) - @asyncio.coroutine - def async_clean_spot(self, **kwargs): + async def async_clean_spot(self, **kwargs): """Perform a spot clean-up.""" - yield from self._try_command( + await self._try_command( "Unable to start the vacuum for a spot clean-up: %s", self._vacuum.spot) - @asyncio.coroutine - def async_locate(self, **kwargs): + async def async_locate(self, **kwargs): """Locate the vacuum cleaner.""" - yield from self._try_command( + await self._try_command( "Unable to locate the botvac: %s", self._vacuum.find) - @asyncio.coroutine - def async_send_command(self, command, params=None, **kwargs): + async def async_send_command(self, command, params=None, **kwargs): """Send raw command.""" - yield from self._try_command( + await self._try_command( "Unable to send command to the vacuum: %s", self._vacuum.raw_command, command, params) - @asyncio.coroutine - def async_remote_control_start(self): + async def async_remote_control_start(self): """Start remote control mode.""" - yield from self._try_command( + await self._try_command( "Unable to start remote control the vacuum: %s", self._vacuum.manual_start) - @asyncio.coroutine - def async_remote_control_stop(self): + async def async_remote_control_stop(self): """Stop remote control mode.""" - yield from self._try_command( + await self._try_command( "Unable to stop remote control the vacuum: %s", self._vacuum.manual_stop) - @asyncio.coroutine - def async_remote_control_move(self, - rotation: int = 0, - velocity: float = 0.3, - duration: int = 1500): + async def async_remote_control_move(self, + rotation: int = 0, + velocity: float = 0.3, + duration: int = 1500): """Move vacuum with remote control mode.""" - yield from self._try_command( + await self._try_command( "Unable to move with remote control the vacuum: %s", self._vacuum.manual_control, velocity=velocity, rotation=rotation, duration=duration) - @asyncio.coroutine - def async_remote_control_move_step(self, - rotation: int = 0, - velocity: float = 0.2, - duration: int = 1500): + async def async_remote_control_move_step(self, + rotation: int = 0, + velocity: float = 0.2, + duration: int = 1500): """Move vacuum one step with remote control mode.""" - yield from self._try_command( + await self._try_command( "Unable to remote control the vacuum: %s", self._vacuum.manual_control_once, velocity=velocity, rotation=rotation, duration=duration) From 9e4c8f45d6e630870605bfcf922b700cc95e59b0 Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Mon, 1 Oct 2018 08:55:43 +0200 Subject: [PATCH 121/247] Async syntax 6, sensor (#17020) --- homeassistant/components/sensor/ads.py | 4 +- .../components/sensor/alarmdecoder.py | 4 +- homeassistant/components/sensor/amcrest.py | 6 +-- .../components/sensor/android_ip_webcam.py | 9 ++-- .../components/sensor/api_streams.py | 6 +-- homeassistant/components/sensor/arwn.py | 8 ++-- homeassistant/components/sensor/bh1750.py | 13 +++--- homeassistant/components/sensor/bme280.py | 15 +++---- homeassistant/components/sensor/bme680.py | 13 +++--- .../components/sensor/bmw_connected_drive.py | 4 +- homeassistant/components/sensor/buienradar.py | 43 ++++++++----------- homeassistant/components/sensor/citybikes.py | 31 ++++++------- .../components/sensor/comed_hourly_pricing.py | 12 +++--- homeassistant/components/sensor/discogs.py | 9 ++-- homeassistant/components/sensor/dnsip.py | 12 +++--- homeassistant/components/sensor/dsmr.py | 19 ++++---- homeassistant/components/sensor/dyson.py | 4 +- homeassistant/components/sensor/envisalink.py | 9 ++-- homeassistant/components/sensor/fail2ban.py | 6 +-- homeassistant/components/sensor/fastdotcom.py | 6 +-- homeassistant/components/sensor/fido.py | 13 +++--- homeassistant/components/sensor/file.py | 6 +-- homeassistant/components/sensor/htu21d.py | 15 +++---- .../components/sensor/hydroquebec.py | 23 ++++------ homeassistant/components/sensor/insteon.py | 6 +-- homeassistant/components/sensor/min_max.py | 9 ++-- homeassistant/components/sensor/mychevy.py | 7 +-- homeassistant/components/sensor/otp.py | 9 ++-- homeassistant/components/sensor/rflink.py | 9 ++-- homeassistant/components/sensor/serial.py | 19 +++----- homeassistant/components/sensor/sma.py | 17 +++----- homeassistant/components/sensor/sochain.py | 11 ++--- homeassistant/components/sensor/speedtest.py | 6 +-- homeassistant/components/sensor/startca.py | 13 +++--- homeassistant/components/sensor/statistics.py | 12 ++---- homeassistant/components/sensor/teksavvy.py | 13 +++--- homeassistant/components/sensor/template.py | 12 ++---- .../components/sensor/thethingsnetwork.py | 19 ++++---- homeassistant/components/sensor/time_date.py | 6 +-- .../components/sensor/viaggiatreno.py | 17 +++----- homeassistant/components/sensor/waqi.py | 14 +++--- .../components/sensor/waterfurnace.py | 4 +- homeassistant/components/sensor/wink.py | 4 +- .../components/sensor/worxlandroid.py | 12 +++--- .../components/sensor/wunderground.py | 5 +-- 45 files changed, 195 insertions(+), 319 deletions(-) diff --git a/homeassistant/components/sensor/ads.py b/homeassistant/components/sensor/ads.py index 5d5cbb379bf..24515357f5e 100644 --- a/homeassistant/components/sensor/ads.py +++ b/homeassistant/components/sensor/ads.py @@ -4,7 +4,6 @@ Support for ADS sensors. For more details about this platform, please refer to the documentation. https://home-assistant.io/components/sensor.ads/ """ -import asyncio import logging import voluptuous as vol @@ -62,8 +61,7 @@ class AdsSensor(Entity): self.ads_type = ads_type self.factor = factor - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Register device notification.""" def update(name, value): """Handle device notifications.""" diff --git a/homeassistant/components/sensor/alarmdecoder.py b/homeassistant/components/sensor/alarmdecoder.py index 51e166bfce6..546d09299dc 100644 --- a/homeassistant/components/sensor/alarmdecoder.py +++ b/homeassistant/components/sensor/alarmdecoder.py @@ -4,7 +4,6 @@ Support for AlarmDecoder Sensors (Shows Panel Display). For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.alarmdecoder/ """ -import asyncio import logging from homeassistant.helpers.entity import Entity @@ -34,8 +33,7 @@ class AlarmDecoderSensor(Entity): self._icon = 'mdi:alarm-check' self._name = 'Alarm Panel Display' - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Register callbacks.""" self.hass.helpers.dispatcher.async_dispatcher_connect( SIGNAL_PANEL_MESSAGE, self._message_callback) diff --git a/homeassistant/components/sensor/amcrest.py b/homeassistant/components/sensor/amcrest.py index 53a8c663f21..50d6e9b7fa9 100644 --- a/homeassistant/components/sensor/amcrest.py +++ b/homeassistant/components/sensor/amcrest.py @@ -4,7 +4,6 @@ This component provides HA sensor support for Amcrest IP cameras. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.amcrest/ """ -import asyncio from datetime import timedelta import logging @@ -19,9 +18,8 @@ _LOGGER = logging.getLogger(__name__) SCAN_INTERVAL = timedelta(seconds=10) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up a sensor for an Amcrest IP Camera.""" if discovery_info is None: return diff --git a/homeassistant/components/sensor/android_ip_webcam.py b/homeassistant/components/sensor/android_ip_webcam.py index 333bf12ec21..0f795f85dcd 100644 --- a/homeassistant/components/sensor/android_ip_webcam.py +++ b/homeassistant/components/sensor/android_ip_webcam.py @@ -4,7 +4,6 @@ Support for IP Webcam sensors. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.android_ip_webcam/ """ -import asyncio from homeassistant.components.android_ip_webcam import ( KEY_MAP, ICON_MAP, DATA_IP_WEBCAM, AndroidIPCamEntity, CONF_HOST, @@ -14,9 +13,8 @@ from homeassistant.helpers.icon import icon_for_battery_level DEPENDENCIES = ['android_ip_webcam'] -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the IP Webcam Sensor.""" if discovery_info is None: return @@ -62,8 +60,7 @@ class IPWebcamSensor(AndroidIPCamEntity): """Return the state of the sensor.""" return self._state - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Retrieve latest state.""" if self._sensor in ('audio_connections', 'video_connections'): if not self._ipcam.status_data: diff --git a/homeassistant/components/sensor/api_streams.py b/homeassistant/components/sensor/api_streams.py index 1ecae1e753e..ac9b15754d0 100644 --- a/homeassistant/components/sensor/api_streams.py +++ b/homeassistant/components/sensor/api_streams.py @@ -4,7 +4,6 @@ Entity to track connections to stream API. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.api_stream/ """ -import asyncio import logging from homeassistant.const import EVENT_HOMEASSISTANT_STOP @@ -48,9 +47,8 @@ class StreamHandler(logging.Handler): self.entity.schedule_update_ha_state() -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the API stream platform.""" entity = APICount() handler = StreamHandler(entity) diff --git a/homeassistant/components/sensor/arwn.py b/homeassistant/components/sensor/arwn.py index 580701490a6..2b79e4c3a9a 100644 --- a/homeassistant/components/sensor/arwn.py +++ b/homeassistant/components/sensor/arwn.py @@ -4,7 +4,6 @@ Support for collecting data from the ARWN project. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.arwn/ """ -import asyncio import json import logging @@ -58,9 +57,8 @@ def _slug(name): return 'sensor.arwn_{}'.format(slugify(name)) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the ARWN platform.""" @callback def async_sensor_event_received(topic, payload, qos): @@ -102,7 +100,7 @@ def async_setup_platform(hass, config, async_add_entities, else: store[sensor.name].set_event(event) - yield from mqtt.async_subscribe( + await mqtt.async_subscribe( hass, TOPIC, async_sensor_event_received, 0) return True diff --git a/homeassistant/components/sensor/bh1750.py b/homeassistant/components/sensor/bh1750.py index 6230ae8a74d..fb0a0116818 100644 --- a/homeassistant/components/sensor/bh1750.py +++ b/homeassistant/components/sensor/bh1750.py @@ -4,7 +4,6 @@ Support for BH1750 light sensor. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.bh1750/ """ -import asyncio from functools import partial import logging @@ -66,9 +65,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ # pylint: disable=import-error -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the BH1750 sensor.""" import smbus from i2csense.bh1750 import BH1750 @@ -80,7 +78,7 @@ def async_setup_platform(hass, config, async_add_entities, bus = smbus.SMBus(bus_number) - sensor = yield from hass.async_add_job( + sensor = await hass.async_add_job( partial(BH1750, bus, i2c_address, operation_mode=operation_mode, measurement_delay=config.get(CONF_DELAY), @@ -133,10 +131,9 @@ class BH1750Sensor(Entity): """Return the class of this device, from component DEVICE_CLASSES.""" return DEVICE_CLASS_ILLUMINANCE - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Get the latest data from the BH1750 and update the states.""" - yield from self.hass.async_add_job(self.bh1750_sensor.update) + await self.hass.async_add_job(self.bh1750_sensor.update) if self.bh1750_sensor.sample_ok \ and self.bh1750_sensor.light_level >= 0: self._state = int(round(self.bh1750_sensor.light_level diff --git a/homeassistant/components/sensor/bme280.py b/homeassistant/components/sensor/bme280.py index 676800c1069..f67dace817e 100644 --- a/homeassistant/components/sensor/bme280.py +++ b/homeassistant/components/sensor/bme280.py @@ -4,7 +4,6 @@ Support for BME280 temperature, humidity and pressure sensor. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.bme280/ """ -import asyncio from datetime import timedelta from functools import partial import logging @@ -81,9 +80,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ # pylint: disable=import-error -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the BME280 sensor.""" import smbus from i2csense.bme280 import BME280 @@ -93,7 +91,7 @@ def async_setup_platform(hass, config, async_add_entities, i2c_address = config.get(CONF_I2C_ADDRESS) bus = smbus.SMBus(config.get(CONF_I2C_BUS)) - sensor = yield from hass.async_add_job( + sensor = await hass.async_add_job( partial(BME280, bus, i2c_address, osrs_t=config.get(CONF_OVERSAMPLING_TEMP), osrs_p=config.get(CONF_OVERSAMPLING_PRES), @@ -108,7 +106,7 @@ def async_setup_platform(hass, config, async_add_entities, _LOGGER.error("BME280 sensor not detected at %s", i2c_address) return False - sensor_handler = yield from hass.async_add_job(BME280Handler, sensor) + sensor_handler = await hass.async_add_job(BME280Handler, sensor) dev = [] try: @@ -163,10 +161,9 @@ class BME280Sensor(Entity): """Return the unit of measurement of the sensor.""" return self._unit_of_measurement - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Get the latest data from the BME280 and update the states.""" - yield from self.hass.async_add_job(self.bme280_client.update) + await self.hass.async_add_job(self.bme280_client.update) if self.bme280_client.sensor.sample_ok: if self.type == SENSOR_TEMP: temperature = round(self.bme280_client.sensor.temperature, 1) diff --git a/homeassistant/components/sensor/bme680.py b/homeassistant/components/sensor/bme680.py index 65d486ada36..c4e8baf6c05 100644 --- a/homeassistant/components/sensor/bme680.py +++ b/homeassistant/components/sensor/bme680.py @@ -7,7 +7,6 @@ Air Quality calculation based on humidity and volatile gas. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.bme680/ """ -import asyncio import logging from time import time, sleep @@ -97,14 +96,13 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the BME680 sensor.""" SENSOR_TYPES[SENSOR_TEMP][1] = hass.config.units.temperature_unit name = config.get(CONF_NAME) - sensor_handler = yield from hass.async_add_job(_setup_bme680, config) + sensor_handler = await hass.async_add_job(_setup_bme680, config) if sensor_handler is None: return @@ -351,10 +349,9 @@ class BME680Sensor(Entity): """Return the unit of measurement of the sensor.""" return self._unit_of_measurement - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Get the latest data from the BME680 and update the states.""" - yield from self.hass.async_add_job(self.bme680_client.update) + await self.hass.async_add_job(self.bme680_client.update) if self.type == SENSOR_TEMP: temperature = round(self.bme680_client.sensor_data.temperature, 1) if self.temp_unit == TEMP_FAHRENHEIT: diff --git a/homeassistant/components/sensor/bmw_connected_drive.py b/homeassistant/components/sensor/bmw_connected_drive.py index ff80100e21d..964a8a4cb16 100644 --- a/homeassistant/components/sensor/bmw_connected_drive.py +++ b/homeassistant/components/sensor/bmw_connected_drive.py @@ -4,7 +4,6 @@ Reads vehicle status from BMW connected drive portal. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.bmw_connected_drive/ """ -import asyncio import logging from homeassistant.components.bmw_connected_drive import DOMAIN as BMW_DOMAIN @@ -124,8 +123,7 @@ class BMWConnectedDriveSensor(Entity): """Schedule a state update.""" self.schedule_update_ha_state(True) - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Add callback after being added to hass. Show latest data after startup. diff --git a/homeassistant/components/sensor/buienradar.py b/homeassistant/components/sensor/buienradar.py index c7ca0c097ff..36585b8e103 100644 --- a/homeassistant/components/sensor/buienradar.py +++ b/homeassistant/components/sensor/buienradar.py @@ -140,9 +140,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Create the buienradar sensor.""" from homeassistant.components.weather.buienradar import DEFAULT_TIMEFRAME @@ -168,7 +167,7 @@ def async_setup_platform(hass, config, async_add_entities, data = BrData(hass, coordinates, timeframe, dev) # schedule the first update in 1 minute from now: - yield from data.schedule_update(1) + await data.schedule_update(1) class BrSensor(Entity): @@ -386,8 +385,7 @@ class BrData: self.coordinates = coordinates self.timeframe = timeframe - @asyncio.coroutine - def update_devices(self): + async def update_devices(self): """Update all devices/sensors.""" if self.devices: tasks = [] @@ -397,18 +395,16 @@ class BrData: tasks.append(dev.async_update_ha_state()) if tasks: - yield from asyncio.wait(tasks, loop=self.hass.loop) + await asyncio.wait(tasks, loop=self.hass.loop) - @asyncio.coroutine - def schedule_update(self, minute=1): + async def schedule_update(self, minute=1): """Schedule an update after minute minutes.""" _LOGGER.debug("Scheduling next update in %s minutes.", minute) nxt = dt_util.utcnow() + timedelta(minutes=minute) async_track_point_in_utc_time(self.hass, self.async_update, nxt) - @asyncio.coroutine - def get_data(self, url): + async def get_data(self, url): """Load data from specified url.""" from buienradar.buienradar import (CONTENT, MESSAGE, STATUS_CODE, SUCCESS) @@ -419,10 +415,10 @@ class BrData: try: websession = async_get_clientsession(self.hass) with async_timeout.timeout(10, loop=self.hass.loop): - resp = yield from websession.get(url) + resp = await websession.get(url) result[STATUS_CODE] = resp.status - result[CONTENT] = yield from resp.text() + result[CONTENT] = await resp.text() if resp.status == 200: result[SUCCESS] = True else: @@ -434,17 +430,16 @@ class BrData: return result finally: if resp is not None: - yield from resp.release() + await resp.release() - @asyncio.coroutine - def async_update(self, *_): + async def async_update(self, *_): """Update the data from buienradar.""" from buienradar.buienradar import (parse_data, CONTENT, DATA, MESSAGE, STATUS_CODE, SUCCESS) - content = yield from self.get_data('http://xml.buienradar.nl') + content = await self.get_data('http://xml.buienradar.nl') if not content.get(SUCCESS, False): - content = yield from self.get_data('http://api.buienradar.nl') + content = await self.get_data('http://api.buienradar.nl') if content.get(SUCCESS) is not True: # unable to get the data @@ -453,7 +448,7 @@ class BrData: content.get(MESSAGE), content.get(STATUS_CODE),) # schedule new call - yield from self.schedule_update(SCHEDULE_NOK) + await self.schedule_update(SCHEDULE_NOK) return # rounding coordinates prevents unnecessary redirects/calls @@ -462,7 +457,7 @@ class BrData: round(self.coordinates[CONF_LATITUDE], 2), round(self.coordinates[CONF_LONGITUDE], 2) ) - raincontent = yield from self.get_data(rainurl) + raincontent = await self.get_data(rainurl) if raincontent.get(SUCCESS) is not True: # unable to get the data @@ -471,7 +466,7 @@ class BrData: raincontent.get(MESSAGE), raincontent.get(STATUS_CODE),) # schedule new call - yield from self.schedule_update(SCHEDULE_NOK) + await self.schedule_update(SCHEDULE_NOK) return result = parse_data(content.get(CONTENT), @@ -486,12 +481,12 @@ class BrData: _LOGGER.warning("Unable to parse data from Buienradar." "(Msg: %s)", result.get(MESSAGE),) - yield from self.schedule_update(SCHEDULE_NOK) + await self.schedule_update(SCHEDULE_NOK) return self.data = result.get(DATA) - yield from self.update_devices() - yield from self.schedule_update(SCHEDULE_OK) + await self.update_devices() + await self.schedule_update(SCHEDULE_OK) @property def attribution(self): diff --git a/homeassistant/components/sensor/citybikes.py b/homeassistant/components/sensor/citybikes.py index 8003a77a452..12c475e62ff 100644 --- a/homeassistant/components/sensor/citybikes.py +++ b/homeassistant/components/sensor/citybikes.py @@ -104,16 +104,15 @@ class CityBikesRequestError(Exception): pass -@asyncio.coroutine -def async_citybikes_request(hass, uri, schema): +async def async_citybikes_request(hass, uri, schema): """Perform a request to CityBikes API endpoint, and parse the response.""" try: session = async_get_clientsession(hass) with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop): - req = yield from session.get(DEFAULT_ENDPOINT.format(uri=uri)) + req = await session.get(DEFAULT_ENDPOINT.format(uri=uri)) - json_response = yield from req.json() + json_response = await req.json() return schema(json_response) except (asyncio.TimeoutError, aiohttp.ClientError): _LOGGER.error("Could not connect to CityBikes API endpoint") @@ -125,9 +124,8 @@ def async_citybikes_request(hass, uri, schema): raise CityBikesRequestError -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the CityBikes platform.""" if PLATFORM not in hass.data: hass.data[PLATFORM] = {MONITORED_NETWORKS: {}} @@ -142,7 +140,7 @@ def async_setup_platform(hass, config, async_add_entities, radius = distance.convert(radius, LENGTH_FEET, LENGTH_METERS) if not network_id: - network_id = yield from CityBikesNetwork.get_closest_network_id( + network_id = await CityBikesNetwork.get_closest_network_id( hass, latitude, longitude) if network_id not in hass.data[PLATFORM][MONITORED_NETWORKS]: @@ -153,7 +151,7 @@ def async_setup_platform(hass, config, async_add_entities, else: network = hass.data[PLATFORM][MONITORED_NETWORKS][network_id] - yield from network.ready.wait() + await network.ready.wait() devices = [] for station in network.stations: @@ -177,13 +175,12 @@ class CityBikesNetwork: NETWORKS_LIST_LOADING = asyncio.Condition() @classmethod - @asyncio.coroutine - def get_closest_network_id(cls, hass, latitude, longitude): + async def get_closest_network_id(cls, hass, latitude, longitude): """Return the id of the network closest to provided location.""" try: - yield from cls.NETWORKS_LIST_LOADING.acquire() + await cls.NETWORKS_LIST_LOADING.acquire() if cls.NETWORKS_LIST is None: - networks = yield from async_citybikes_request( + networks = await async_citybikes_request( hass, NETWORKS_URI, NETWORKS_RESPONSE_SCHEMA) cls.NETWORKS_LIST = networks[ATTR_NETWORKS_LIST] result = None @@ -210,11 +207,10 @@ class CityBikesNetwork: self.stations = [] self.ready = asyncio.Event() - @asyncio.coroutine - def async_refresh(self, now=None): + async def async_refresh(self, now=None): """Refresh the state of the network.""" try: - network = yield from async_citybikes_request( + network = await async_citybikes_request( self.hass, STATIONS_URI.format(uid=self.network_id), STATIONS_RESPONSE_SCHEMA) self.stations = network[ATTR_NETWORK][ATTR_STATIONS_LIST] @@ -253,8 +249,7 @@ class CityBikesStation(Entity): return self._station_data[ATTR_NAME] return None - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Update station state.""" if self._network.ready.is_set(): for station in self._network.stations: diff --git a/homeassistant/components/sensor/comed_hourly_pricing.py b/homeassistant/components/sensor/comed_hourly_pricing.py index 3595bcaa227..b5d230d8517 100644 --- a/homeassistant/components/sensor/comed_hourly_pricing.py +++ b/homeassistant/components/sensor/comed_hourly_pricing.py @@ -49,9 +49,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the ComEd Hourly Pricing sensor.""" websession = async_get_clientsession(hass) dev = [] @@ -101,8 +100,7 @@ class ComedHourlyPricingSensor(Entity): attrs = {ATTR_ATTRIBUTION: CONF_ATTRIBUTION} return attrs - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Get the ComEd Hourly Pricing data from the web service.""" try: if self.type == CONF_FIVE_MINUTE or \ @@ -114,9 +112,9 @@ class ComedHourlyPricingSensor(Entity): url_string += '?type=currenthouraverage' with async_timeout.timeout(60, loop=self.loop): - response = yield from self.websession.get(url_string) + response = await self.websession.get(url_string) # The API responds with MIME type 'text/html' - text = yield from response.text() + text = await response.text() data = json.loads(text) self._state = round( float(data[0]['price']) + self.offset, 2) diff --git a/homeassistant/components/sensor/discogs.py b/homeassistant/components/sensor/discogs.py index 6e85c41ac6e..70d7155fec7 100644 --- a/homeassistant/components/sensor/discogs.py +++ b/homeassistant/components/sensor/discogs.py @@ -4,7 +4,6 @@ Show the amount of records in a user's Discogs collection. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.discogs/ """ -import asyncio from datetime import timedelta import logging @@ -36,9 +35,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the Discogs sensor.""" import discogs_client @@ -92,7 +90,6 @@ class DiscogsSensor(Entity): ATTR_IDENTITY: self._identity.name, } - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Set state to the amount of records in user's collection.""" self._state = self._identity.num_collection diff --git a/homeassistant/components/sensor/dnsip.py b/homeassistant/components/sensor/dnsip.py index 3027b6f8ca6..7b0d54cd934 100644 --- a/homeassistant/components/sensor/dnsip.py +++ b/homeassistant/components/sensor/dnsip.py @@ -4,7 +4,6 @@ Get your own public IP address or that of any host. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.dnsip/ """ -import asyncio import logging from datetime import timedelta @@ -42,8 +41,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_devices, discovery_info=None): +async def async_setup_platform(hass, config, async_add_devices, + discovery_info=None): """Set up the DNS IP sensor.""" hostname = config.get(CONF_HOSTNAME) name = config.get(CONF_NAME) @@ -86,11 +85,10 @@ class WanIpSensor(Entity): """Return the current DNS IP address for hostname.""" return self._state - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Get the current DNS IP address for hostname.""" - response = yield from self.resolver.query(self.hostname, - self.querytype) + response = await self.resolver.query(self.hostname, + self.querytype) if response: self._state = response[0].host else: diff --git a/homeassistant/components/sensor/dsmr.py b/homeassistant/components/sensor/dsmr.py index 13b13114150..02b2a5e8ff5 100644 --- a/homeassistant/components/sensor/dsmr.py +++ b/homeassistant/components/sensor/dsmr.py @@ -48,9 +48,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the DSMR sensor.""" # Suppress logging logging.getLogger('dsmr_parser').setLevel(logging.ERROR) @@ -182,13 +181,12 @@ def async_setup_platform(hass, config, async_add_entities, create_dsmr_reader, config[CONF_PORT], config[CONF_DSMR_VERSION], update_entities_telegram, loop=hass.loop) - @asyncio.coroutine - def connect_and_reconnect(): + async def connect_and_reconnect(): """Connect to DSMR and keep reconnecting until Home Assistant stops.""" while hass.state != CoreState.stopping: # Start DSMR asyncio.Protocol reader try: - transport, protocol = yield from hass.loop.create_task( + transport, protocol = await hass.loop.create_task( reader_factory()) except (serial.serialutil.SerialException, ConnectionRefusedError, TimeoutError): @@ -203,7 +201,7 @@ def async_setup_platform(hass, config, async_add_entities, EVENT_HOMEASSISTANT_STOP, transport.close) # Wait for reader to close - yield from protocol.wait_closed() + await protocol.wait_closed() if hass.state != CoreState.stopping: # Unexpected disconnect @@ -216,8 +214,8 @@ def async_setup_platform(hass, config, async_add_entities, update_entities_telegram({}) # throttle reconnect attempts - yield from asyncio.sleep(config[CONF_RECONNECT_INTERVAL], - loop=hass.loop) + await asyncio.sleep(config[CONF_RECONNECT_INTERVAL], + loop=hass.loop) # Can't be hass.async_add_job because job runs forever hass.loop.create_task(connect_and_reconnect()) @@ -309,8 +307,7 @@ class DerivativeDSMREntity(DSMREntity): """Return the calculated current hourly rate.""" return self._state - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Recalculate hourly rate if timestamp has changed. DSMR updates gas meter reading every hour. Along with the new diff --git a/homeassistant/components/sensor/dyson.py b/homeassistant/components/sensor/dyson.py index 4097bff32bf..53913d47b72 100644 --- a/homeassistant/components/sensor/dyson.py +++ b/homeassistant/components/sensor/dyson.py @@ -4,7 +4,6 @@ Support for Dyson Pure Cool Link Sensors. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.dyson/ """ -import asyncio import logging from homeassistant.components.dyson import DYSON_DEVICES @@ -58,8 +57,7 @@ class DysonSensor(Entity): self._name = None self._sensor_type = sensor_type - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Call when entity is added to hass.""" self.hass.async_add_job( self._device.add_message_listener, self.on_message) diff --git a/homeassistant/components/sensor/envisalink.py b/homeassistant/components/sensor/envisalink.py index 91f99e31b48..aed2b056d2f 100644 --- a/homeassistant/components/sensor/envisalink.py +++ b/homeassistant/components/sensor/envisalink.py @@ -4,7 +4,6 @@ Support for Envisalink sensors (shows panel info). For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.envisalink/ """ -import asyncio import logging from homeassistant.core import callback @@ -19,9 +18,8 @@ _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['envisalink'] -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Perform the setup for Envisalink sensor devices.""" configured_partitions = discovery_info['partitions'] @@ -51,8 +49,7 @@ class EnvisalinkSensor(EnvisalinkDevice, Entity): _LOGGER.debug("Setting up sensor for partition: %s", partition_name) super().__init__(partition_name + ' Keypad', info, controller) - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Register callbacks.""" async_dispatcher_connect( self.hass, SIGNAL_KEYPAD_UPDATE, self._update_callback) diff --git a/homeassistant/components/sensor/fail2ban.py b/homeassistant/components/sensor/fail2ban.py index 0f018af819d..f152a43e241 100644 --- a/homeassistant/components/sensor/fail2ban.py +++ b/homeassistant/components/sensor/fail2ban.py @@ -5,7 +5,6 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.fail2ban/ """ import os -import asyncio import logging from datetime import timedelta @@ -39,9 +38,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the fail2ban sensor.""" name = config.get(CONF_NAME) jails = config.get(CONF_JAILS) diff --git a/homeassistant/components/sensor/fastdotcom.py b/homeassistant/components/sensor/fastdotcom.py index 6624265f60c..c6a56701f7c 100644 --- a/homeassistant/components/sensor/fastdotcom.py +++ b/homeassistant/components/sensor/fastdotcom.py @@ -4,7 +4,6 @@ Support for Fast.com internet speed testing sensor. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.fastdotcom/ """ -import asyncio import logging import voluptuous as vol @@ -88,10 +87,9 @@ class SpeedtestSensor(Entity): self._state = data['download'] - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Handle entity which will be added.""" - state = yield from async_get_last_state(self.hass, self.entity_id) + state = await async_get_last_state(self.hass, self.entity_id) if not state: return self._state = state.state diff --git a/homeassistant/components/sensor/fido.py b/homeassistant/components/sensor/fido.py index 4c027b906a2..00754c5ba68 100644 --- a/homeassistant/components/sensor/fido.py +++ b/homeassistant/components/sensor/fido.py @@ -7,7 +7,6 @@ https://www.fido.ca/pages/#/my-account/wireless For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.fido/ """ -import asyncio import logging from datetime import timedelta @@ -70,16 +69,15 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the Fido sensor.""" username = config.get(CONF_USERNAME) password = config.get(CONF_PASSWORD) httpsession = hass.helpers.aiohttp_client.async_get_clientsession() fido_data = FidoData(username, password, httpsession) - ret = yield from fido_data.async_update() + ret = await fido_data.async_update() if ret is False: return @@ -134,10 +132,9 @@ class FidoSensor(Entity): 'number': self._number, } - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Get the latest data from Fido and update the state.""" - yield from self.fido_data.async_update() + await self.fido_data.async_update() if self.type == 'balance': if self.fido_data.data.get(self.type) is not None: self._state = round(self.fido_data.data[self.type], 2) diff --git a/homeassistant/components/sensor/file.py b/homeassistant/components/sensor/file.py index 1839b3566ee..3e2a5c21be8 100644 --- a/homeassistant/components/sensor/file.py +++ b/homeassistant/components/sensor/file.py @@ -5,7 +5,6 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.file/ """ import os -import asyncio import logging import voluptuous as vol @@ -32,9 +31,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the file sensor.""" file_path = config.get(CONF_FILE_PATH) name = config.get(CONF_NAME) diff --git a/homeassistant/components/sensor/htu21d.py b/homeassistant/components/sensor/htu21d.py index 28ab933ff6c..ae2555f57f9 100644 --- a/homeassistant/components/sensor/htu21d.py +++ b/homeassistant/components/sensor/htu21d.py @@ -4,7 +4,6 @@ Support for HTU21D temperature and humidity sensor. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.htu21d/ """ -import asyncio from datetime import timedelta from functools import partial import logging @@ -40,9 +39,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ # pylint: disable=import-error -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the HTU21D sensor.""" import smbus from i2csense.htu21d import HTU21D @@ -52,14 +50,14 @@ def async_setup_platform(hass, config, async_add_entities, temp_unit = hass.config.units.temperature_unit bus = smbus.SMBus(config.get(CONF_I2C_BUS)) - sensor = yield from hass.async_add_job( + sensor = await hass.async_add_job( partial(HTU21D, bus, logger=_LOGGER) ) if not sensor.sample_ok: _LOGGER.error("HTU21D sensor not detected in bus %s", bus_number) return False - sensor_handler = yield from hass.async_add_job(HTU21DHandler, sensor) + sensor_handler = await hass.async_add_job(HTU21DHandler, sensor) dev = [HTU21DSensor(sensor_handler, name, SENSOR_TEMPERATURE, temp_unit), HTU21DSensor(sensor_handler, name, SENSOR_HUMIDITY, '%')] @@ -107,10 +105,9 @@ class HTU21DSensor(Entity): """Return the unit of measurement of the sensor.""" return self._unit_of_measurement - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Get the latest data from the HTU21D sensor and update the state.""" - yield from self.hass.async_add_job(self._client.update) + await self.hass.async_add_job(self._client.update) if self._client.sensor.sample_ok: if self._variable == SENSOR_TEMPERATURE: value = round(self._client.sensor.temperature, 1) diff --git a/homeassistant/components/sensor/hydroquebec.py b/homeassistant/components/sensor/hydroquebec.py index 44b96bab1e9..cb75e69b919 100644 --- a/homeassistant/components/sensor/hydroquebec.py +++ b/homeassistant/components/sensor/hydroquebec.py @@ -7,7 +7,6 @@ https://www.hydroquebec.com/portail/en/group/clientele/portrait-de-consommation For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.hydroquebec/ """ -import asyncio import logging from datetime import timedelta @@ -93,9 +92,8 @@ DAILY_MAP = (('yesterday_total_consumption', 'consoTotalQuot'), ('yesterday_higher_price_consumption', 'consoHautQuot')) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the HydroQuebec sensor.""" # Create a data fetcher to support all of the configured sensors. Then make # the first call to init the data. @@ -107,7 +105,7 @@ def async_setup_platform(hass, config, async_add_entities, httpsession = hass.helpers.aiohttp_client.async_get_clientsession() hydroquebec_data = HydroquebecData(username, password, httpsession, contract) - contracts = yield from hydroquebec_data.get_contract_list() + contracts = await hydroquebec_data.get_contract_list() if not contracts: return _LOGGER.info("Contract list: %s", @@ -155,10 +153,9 @@ class HydroQuebecSensor(Entity): """Icon to use in the frontend, if any.""" return self._icon - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Get the latest data from Hydroquebec and update the state.""" - yield from self.hydroquebec_data.async_update() + await self.hydroquebec_data.async_update() if self.hydroquebec_data.data.get(self.type) is not None: self._state = round(self.hydroquebec_data.data[self.type], 2) @@ -174,11 +171,10 @@ class HydroquebecData: self._contract = contract self.data = {} - @asyncio.coroutine - def get_contract_list(self): + async def get_contract_list(self): """Return the contract list.""" # Fetch data - ret = yield from self._fetch_data() + ret = await self._fetch_data() if ret: return self.client.get_contracts() return [] @@ -194,8 +190,7 @@ class HydroquebecData: return False return True - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Return the latest collected data from HydroQuebec.""" - yield from self._fetch_data() + await self._fetch_data() self.data = self.client.get_data(self._contract)[self._contract] diff --git a/homeassistant/components/sensor/insteon.py b/homeassistant/components/sensor/insteon.py index 5b8a6b9a977..7854967395b 100644 --- a/homeassistant/components/sensor/insteon.py +++ b/homeassistant/components/sensor/insteon.py @@ -4,7 +4,6 @@ Support for INSTEON dimmers via PowerLinc Modem. For more details about this component, please refer to the documentation at https://home-assistant.io/components/sensor.insteon/ """ -import asyncio import logging from homeassistant.components.insteon import InsteonEntity @@ -15,9 +14,8 @@ DEPENDENCIES = ['insteon'] _LOGGER = logging.getLogger(__name__) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the INSTEON device class for the hass platform.""" insteon_modem = hass.data['insteon'].get('modem') diff --git a/homeassistant/components/sensor/min_max.py b/homeassistant/components/sensor/min_max.py index 7956dd97b5e..7d9e91a1bf1 100644 --- a/homeassistant/components/sensor/min_max.py +++ b/homeassistant/components/sensor/min_max.py @@ -4,7 +4,6 @@ Support for displaying the minimal and the maximal value. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.min_max/ """ -import asyncio import logging import voluptuous as vol @@ -54,9 +53,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the min/max/mean sensor.""" entity_ids = config.get(CONF_ENTITY_IDS) name = config.get(CONF_NAME) @@ -194,8 +192,7 @@ class MinMaxSensor(Entity): """Return the icon to use in the frontend, if any.""" return ICON - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Get the latest data and updates the states.""" sensor_values = [self.states[k] for k in self._entity_ids if k in self.states] diff --git a/homeassistant/components/sensor/mychevy.py b/homeassistant/components/sensor/mychevy.py index fa3343d7791..06d4dade6aa 100644 --- a/homeassistant/components/sensor/mychevy.py +++ b/homeassistant/components/sensor/mychevy.py @@ -4,7 +4,6 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.mychevy/ """ -import asyncio import logging from homeassistant.components.mychevy import ( @@ -55,8 +54,7 @@ class MyChevyStatus(Entity): """Initialize sensor with car connection.""" self._state = None - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Register callbacks.""" self.hass.helpers.dispatcher.async_dispatcher_connect( UPDATE_TOPIC, self.success) @@ -129,8 +127,7 @@ class EVSensor(Entity): slugify(self._car.name), slugify(self._name))) - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Register callbacks.""" self.hass.helpers.dispatcher.async_dispatcher_connect( UPDATE_TOPIC, self.async_update_callback) diff --git a/homeassistant/components/sensor/otp.py b/homeassistant/components/sensor/otp.py index 2e3a13928e1..5394b49c389 100644 --- a/homeassistant/components/sensor/otp.py +++ b/homeassistant/components/sensor/otp.py @@ -5,7 +5,6 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.otp/ """ import time -import asyncio import logging import voluptuous as vol @@ -32,9 +31,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the OTP sensor.""" name = config.get(CONF_NAME) token = config.get(CONF_TOKEN) @@ -55,8 +53,7 @@ class TOTPSensor(Entity): self._state = None self._next_expiration = None - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Handle when an entity is about to be added to Home Assistant.""" self._call_loop() diff --git a/homeassistant/components/sensor/rflink.py b/homeassistant/components/sensor/rflink.py index 3952c815dca..e01c441be84 100644 --- a/homeassistant/components/sensor/rflink.py +++ b/homeassistant/components/sensor/rflink.py @@ -4,7 +4,6 @@ Support for Rflink sensors. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/light.rflink/ """ -import asyncio from functools import partial import logging @@ -74,14 +73,12 @@ def devices_from_config(domain_config, hass=None): return devices -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the Rflink platform.""" async_add_entities(devices_from_config(config, hass)) - @asyncio.coroutine - def add_new_device(event): + async def add_new_device(event): """Check if device is known, otherwise create device entity.""" device_id = event[EVENT_KEY_ID] diff --git a/homeassistant/components/sensor/serial.py b/homeassistant/components/sensor/serial.py index 39b69e0a8c4..5d49b065558 100644 --- a/homeassistant/components/sensor/serial.py +++ b/homeassistant/components/sensor/serial.py @@ -4,7 +4,6 @@ Support for reading data from a serial port. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.serial/ """ -import asyncio import logging import json @@ -35,9 +34,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the Serial sensor platform.""" name = config.get(CONF_NAME) port = config.get(CONF_SERIAL_PORT) @@ -67,20 +65,18 @@ class SerialSensor(Entity): self._template = value_template self._attributes = [] - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Handle when an entity is about to be added to Home Assistant.""" self._serial_loop_task = self.hass.loop.create_task( self.serial_read(self._port, self._baudrate)) - @asyncio.coroutine - def serial_read(self, device, rate, **kwargs): + async def serial_read(self, device, rate, **kwargs): """Read the data from the port.""" import serial_asyncio - reader, _ = yield from serial_asyncio.open_serial_connection( + reader, _ = await serial_asyncio.open_serial_connection( url=device, baudrate=rate, **kwargs) while True: - line = yield from reader.readline() + line = await reader.readline() line = line.decode('utf-8').strip() try: @@ -98,8 +94,7 @@ class SerialSensor(Entity): self._state = line self.async_schedule_update_ha_state() - @asyncio.coroutine - def stop_serial_read(self): + async def stop_serial_read(self): """Close resources.""" if self._serial_loop_task: self._serial_loop_task.cancel() diff --git a/homeassistant/components/sensor/sma.py b/homeassistant/components/sensor/sma.py index 945c3873bb6..dd5209a4e0a 100644 --- a/homeassistant/components/sensor/sma.py +++ b/homeassistant/components/sensor/sma.py @@ -63,9 +63,8 @@ PLATFORM_SCHEMA = vol.All(PLATFORM_SCHEMA.extend({ }, extra=vol.PREVENT_EXTRA), _check_sensor_schema) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up SMA WebConnect sensor.""" import pysma @@ -107,10 +106,9 @@ def async_setup_platform(hass, config, async_add_entities, sma = pysma.SMA(session, url, config[CONF_PASSWORD], group=grp) # Ensure we logout on shutdown - @asyncio.coroutine - def async_close_session(event): + async def async_close_session(event): """Close the session.""" - yield from sma.close_session() + await sma.close_session() hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, async_close_session) @@ -120,15 +118,14 @@ def async_setup_platform(hass, config, async_add_entities, backoff = 0 - @asyncio.coroutine - def async_sma(event): + async def async_sma(event): """Update all the SMA sensors.""" nonlocal backoff if backoff > 1: backoff -= 1 return - values = yield from sma.read(keys_to_query) + values = await sma.read(keys_to_query) if values is None: backoff = 3 return @@ -142,7 +139,7 @@ def async_setup_platform(hass, config, async_add_entities, if task: tasks.append(task) if tasks: - yield from asyncio.wait(tasks, loop=hass.loop) + await asyncio.wait(tasks, loop=hass.loop) interval = config.get(CONF_SCAN_INTERVAL) or timedelta(seconds=5) async_track_time_interval(hass, async_sma, interval) diff --git a/homeassistant/components/sensor/sochain.py b/homeassistant/components/sensor/sochain.py index 9f8982f871f..b582ba04567 100644 --- a/homeassistant/components/sensor/sochain.py +++ b/homeassistant/components/sensor/sochain.py @@ -4,7 +4,6 @@ Support for watching multiple cryptocurrencies. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.sochain/ """ -import asyncio import logging from datetime import timedelta @@ -35,9 +34,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the sochain sensors.""" from pysochain import ChainSo address = config.get(CONF_ADDRESS) @@ -82,7 +80,6 @@ class SochainSensor(Entity): ATTR_ATTRIBUTION: CONF_ATTRIBUTION, } - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Get the latest state of the sensor.""" - yield from self.chainso.async_get_data() + await self.chainso.async_get_data() diff --git a/homeassistant/components/sensor/speedtest.py b/homeassistant/components/sensor/speedtest.py index 8da7374f231..ee6cad61e20 100644 --- a/homeassistant/components/sensor/speedtest.py +++ b/homeassistant/components/sensor/speedtest.py @@ -4,7 +4,6 @@ Support for Speedtest.net. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.speedtest/ """ -import asyncio import logging import voluptuous as vol @@ -139,10 +138,9 @@ class SpeedtestSensor(Entity): elif self.type == 'upload': self._state = round(self._data['upload'] / 10**6, 2) - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Handle all entity which are about to be added.""" - state = yield from async_get_last_state(self.hass, self.entity_id) + state = await async_get_last_state(self.hass, self.entity_id) if not state: return self._state = state.state diff --git a/homeassistant/components/sensor/startca.py b/homeassistant/components/sensor/startca.py index d9a52e4aa23..85939ea72ae 100644 --- a/homeassistant/components/sensor/startca.py +++ b/homeassistant/components/sensor/startca.py @@ -7,7 +7,6 @@ https://home-assistant.io/components/sensor.startca/ from datetime import timedelta from xml.parsers.expat import ExpatError import logging -import asyncio import async_timeout import voluptuous as vol @@ -57,16 +56,15 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the sensor platform.""" websession = async_get_clientsession(hass) apikey = config.get(CONF_API_KEY) bandwidthcap = config.get(CONF_TOTAL_BANDWIDTH) ts_data = StartcaData(hass.loop, websession, apikey, bandwidthcap) - ret = yield from ts_data.async_update() + ret = await ts_data.async_update() if ret is False: _LOGGER.error("Invalid Start.ca API key: %s", apikey) return @@ -111,10 +109,9 @@ class StartcaSensor(Entity): """Icon to use in the frontend, if any.""" return self._icon - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Get the latest data from Start.ca and update the state.""" - yield from self.startcadata.async_update() + await self.startcadata.async_update() if self.type in self.startcadata.data: self._state = round(self.startcadata.data[self.type], 2) diff --git a/homeassistant/components/sensor/statistics.py b/homeassistant/components/sensor/statistics.py index e7692001ffa..453acb94b11 100644 --- a/homeassistant/components/sensor/statistics.py +++ b/homeassistant/components/sensor/statistics.py @@ -4,7 +4,6 @@ Support for statistics for sensor values. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.statistics/ """ -import asyncio import logging import statistics from collections import deque @@ -58,9 +57,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the Statistics sensor.""" entity_id = config.get(CONF_ENTITY_ID) name = config.get(CONF_NAME) @@ -179,8 +177,7 @@ class StatisticsSensor(Entity): self.ages.popleft() self.states.popleft() - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Get the latest data and updates the states.""" if self._max_age is not None: self._purge_old() @@ -236,8 +233,7 @@ class StatisticsSensor(Entity): self.change = self.average_change = STATE_UNKNOWN self.change_rate = STATE_UNKNOWN - @asyncio.coroutine - def _initialize_from_database(self): + async def _initialize_from_database(self): """Initialize the list of states from the database. The query will get the list of states in DESCENDING order so that we diff --git a/homeassistant/components/sensor/teksavvy.py b/homeassistant/components/sensor/teksavvy.py index 87b89074cfb..0be18cbd6b6 100644 --- a/homeassistant/components/sensor/teksavvy.py +++ b/homeassistant/components/sensor/teksavvy.py @@ -6,7 +6,6 @@ https://home-assistant.io/components/sensor.teksavvy/ """ from datetime import timedelta import logging -import asyncio import async_timeout import voluptuous as vol @@ -58,16 +57,15 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the sensor platform.""" websession = async_get_clientsession(hass) apikey = config.get(CONF_API_KEY) bandwidthcap = config.get(CONF_TOTAL_BANDWIDTH) ts_data = TekSavvyData(hass.loop, websession, apikey, bandwidthcap) - ret = yield from ts_data.async_update() + ret = await ts_data.async_update() if ret is False: _LOGGER.error("Invalid Teksavvy API key: %s", apikey) return @@ -112,10 +110,9 @@ class TekSavvySensor(Entity): """Icon to use in the frontend, if any.""" return self._icon - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Get the latest data from TekSavvy and update the state.""" - yield from self.teksavvydata.async_update() + await self.teksavvydata.async_update() if self.type in self.teksavvydata.data: self._state = round(self.teksavvydata.data[self.type], 2) diff --git a/homeassistant/components/sensor/template.py b/homeassistant/components/sensor/template.py index f64e8b122ca..77b3759d5fc 100644 --- a/homeassistant/components/sensor/template.py +++ b/homeassistant/components/sensor/template.py @@ -4,7 +4,6 @@ Allows the creation of a sensor that breaks out state_attributes. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.template/ """ -import asyncio import logging from typing import Optional @@ -41,9 +40,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the template sensors.""" sensors = [] @@ -123,8 +121,7 @@ class SensorTemplate(Entity): self._entities = entity_ids self._device_class = device_class - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Register callbacks.""" @callback def template_sensor_state_listener(entity, old_state, new_state): @@ -177,8 +174,7 @@ class SensorTemplate(Entity): """No polling needed.""" return False - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Update the state from the template.""" try: self._state = self._template.async_render() diff --git a/homeassistant/components/sensor/thethingsnetwork.py b/homeassistant/components/sensor/thethingsnetwork.py index 02661f2211d..0f1220f9b07 100644 --- a/homeassistant/components/sensor/thethingsnetwork.py +++ b/homeassistant/components/sensor/thethingsnetwork.py @@ -38,9 +38,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up The Things Network Data storage sensors.""" ttn = hass.data.get(DATA_TTN) device_id = config.get(CONF_DEVICE_ID) @@ -50,7 +49,7 @@ def async_setup_platform(hass, config, async_add_entities, ttn_data_storage = TtnDataStorage( hass, app_id, device_id, access_key, values) - success = yield from ttn_data_storage.async_update() + success = await ttn_data_storage.async_update() if not success: return False @@ -104,10 +103,9 @@ class TtnDataSensor(Entity): ATTR_TIME: self._state['time'], } - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Get the current state.""" - yield from self._ttn_data_storage.async_update() + await self._ttn_data_storage.async_update() self._state = self._ttn_data_storage.data @@ -128,13 +126,12 @@ class TtnDataStorage: AUTHORIZATION: 'key {}'.format(access_key), } - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Get the current state from The Things Network Data Storage.""" try: session = async_get_clientsession(self._hass) with async_timeout.timeout(DEFAULT_TIMEOUT, loop=self._hass.loop): - req = yield from session.get(self._url, headers=self._headers) + req = await session.get(self._url, headers=self._headers) except (asyncio.TimeoutError, aiohttp.ClientError): _LOGGER.error("Error while accessing: %s", self._url) @@ -155,7 +152,7 @@ class TtnDataStorage: _LOGGER.error("Application ID is not available: %s", self._app_id) return False - data = yield from req.json() + data = await req.json() self.data = data[0] for value in self._values.items(): diff --git a/homeassistant/components/sensor/time_date.py b/homeassistant/components/sensor/time_date.py index e4c719acd0d..1b346d409c4 100644 --- a/homeassistant/components/sensor/time_date.py +++ b/homeassistant/components/sensor/time_date.py @@ -5,7 +5,6 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.time_date/ """ from datetime import timedelta -import asyncio import logging import voluptuous as vol @@ -37,9 +36,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the Time and Date sensor.""" if hass.config.time_zone is None: _LOGGER.error("Timezone is not set in Home Assistant configuration") diff --git a/homeassistant/components/sensor/viaggiatreno.py b/homeassistant/components/sensor/viaggiatreno.py index 1dd8523eb4b..82068c456b6 100644 --- a/homeassistant/components/sensor/viaggiatreno.py +++ b/homeassistant/components/sensor/viaggiatreno.py @@ -56,9 +56,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the ViaggiaTreno platform.""" train_id = config.get(CONF_TRAIN_ID) station_id = config.get(CONF_STATION_ID) @@ -68,16 +67,15 @@ def async_setup_platform(hass, config, async_add_entities, async_add_entities([ViaggiaTrenoSensor(train_id, station_id, name)]) -@asyncio.coroutine -def async_http_request(hass, uri): +async def async_http_request(hass, uri): """Perform actual request.""" try: session = hass.helpers.aiohttp_client.async_get_clientsession(hass) with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop): - req = yield from session.get(uri) + req = await session.get(uri) if req.status != 200: return {'error': req.status} - json_response = yield from req.json() + json_response = await req.json() return json_response except (asyncio.TimeoutError, aiohttp.ClientError) as exc: _LOGGER.error("Cannot connect to ViaggiaTreno API endpoint: %s", exc) @@ -152,11 +150,10 @@ class ViaggiaTrenoSensor(Entity): return True return False - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Update state.""" uri = self.uri - res = yield from async_http_request(self.hass, uri) + res = await async_http_request(self.hass, uri) if res.get('error', ''): if res['error'] == 204: self._state = NO_INFORMATION_STRING diff --git a/homeassistant/components/sensor/waqi.py b/homeassistant/components/sensor/waqi.py index 9f90f465fb2..d6b8d278fb1 100644 --- a/homeassistant/components/sensor/waqi.py +++ b/homeassistant/components/sensor/waqi.py @@ -59,9 +59,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the requested World Air Quality Index locations.""" import waqiasync @@ -74,7 +73,7 @@ def async_setup_platform(hass, config, async_add_entities, dev = [] try: for location_name in locations: - stations = yield from client.search(location_name) + stations = await client.search(location_name) _LOGGER.debug("The following stations were returned: %s", stations) for station in stations: waqi_sensor = WaqiSensor(client, station) @@ -161,13 +160,12 @@ class WaqiSensor(Entity): except (IndexError, KeyError): return {ATTR_ATTRIBUTION: ATTRIBUTION} - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Get the latest data and updates the states.""" if self.uid: - result = yield from self._client.get_station_by_number(self.uid) + result = await self._client.get_station_by_number(self.uid) elif self.url: - result = yield from self._client.get_station_by_name(self.url) + result = await self._client.get_station_by_name(self.url) else: result = None self._data = result diff --git a/homeassistant/components/sensor/waterfurnace.py b/homeassistant/components/sensor/waterfurnace.py index 806b40551df..60da761cf75 100644 --- a/homeassistant/components/sensor/waterfurnace.py +++ b/homeassistant/components/sensor/waterfurnace.py @@ -4,7 +4,6 @@ Support for Waterfurnace. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.waterfurnace/ """ -import asyncio from homeassistant.components.sensor import ENTITY_ID_FORMAT from homeassistant.components.waterfurnace import ( @@ -100,8 +99,7 @@ class WaterFurnaceSensor(Entity): """Return the polling state.""" return False - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Register callbacks.""" self.hass.helpers.dispatcher.async_dispatcher_connect( UPDATE_TOPIC, self.async_update_callback) diff --git a/homeassistant/components/sensor/wink.py b/homeassistant/components/sensor/wink.py index 8e11b054b24..8c2abc0f875 100644 --- a/homeassistant/components/sensor/wink.py +++ b/homeassistant/components/sensor/wink.py @@ -4,7 +4,6 @@ Support for Wink sensors. For more details about this platform, please refer to the documentation at at https://home-assistant.io/components/sensor.wink/ """ -import asyncio import logging from homeassistant.components.wink import DOMAIN, WinkDevice @@ -60,8 +59,7 @@ class WinkSensorDevice(WinkDevice, Entity): else: self._unit_of_measurement = self.wink.unit() - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Call when entity is added to hass.""" self.hass.data[DOMAIN]['entities']['sensor'].append(self) diff --git a/homeassistant/components/sensor/worxlandroid.py b/homeassistant/components/sensor/worxlandroid.py index f6593f4b1c5..be5c8452d88 100644 --- a/homeassistant/components/sensor/worxlandroid.py +++ b/homeassistant/components/sensor/worxlandroid.py @@ -50,9 +50,8 @@ ERROR_STATE = [ ] -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the Worx Landroid sensors.""" for typ in ('battery', 'state'): async_add_entities([WorxLandroidSensor(typ, config)]) @@ -88,8 +87,7 @@ class WorxLandroidSensor(Entity): return '%' return None - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Update the sensor data from the mower.""" connection_error = False @@ -97,7 +95,7 @@ class WorxLandroidSensor(Entity): session = async_get_clientsession(self.hass) with async_timeout.timeout(self.timeout, loop=self.hass.loop): auth = aiohttp.helpers.BasicAuth('admin', self.pin) - mower_response = yield from session.get(self.url, auth=auth) + mower_response = await session.get(self.url, auth=auth) except (asyncio.TimeoutError, aiohttp.ClientError): if self.allow_unreachable is False: _LOGGER.error("Error connecting to mower at %s", self.url) @@ -115,7 +113,7 @@ class WorxLandroidSensor(Entity): elif connection_error is False: # set the expected content type to be text/html # since the mover incorrectly returns it... - data = yield from mower_response.json(content_type='text/html') + data = await mower_response.json(content_type='text/html') # sensor battery if self.sensor == 'battery': diff --git a/homeassistant/components/sensor/wunderground.py b/homeassistant/components/sensor/wunderground.py index a14d4b94789..590a9d96b4d 100644 --- a/homeassistant/components/sensor/wunderground.py +++ b/homeassistant/components/sensor/wunderground.py @@ -741,10 +741,9 @@ class WUndergroundSensor(Entity): """Return the units of measurement.""" return self._unit_of_measurement - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Update current conditions.""" - yield from self.rest.async_update() + await self.rest.async_update() if not self.rest.data: # no data, return From 3b5e5cbcd60d40227c741a145dd5917bc2d17e9b Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Mon, 1 Oct 2018 08:56:50 +0200 Subject: [PATCH 122/247] Async syntax 5, light & lock & remote & scene & telegram & helpers (#17019) --- homeassistant/components/light/ads.py | 4 +-- homeassistant/components/light/insteon.py | 12 +++---- .../components/light/limitlessled.py | 6 ++-- .../components/light/lutron_caseta.py | 15 +++------ homeassistant/components/light/rflink.py | 31 +++++++------------ homeassistant/components/light/template.py | 22 +++++-------- homeassistant/components/light/wemo.py | 8 ++--- homeassistant/components/light/wink.py | 4 +-- homeassistant/components/lock/__init__.py | 6 ++-- .../components/lock/bmw_connected_drive.py | 4 +-- homeassistant/components/lock/mqtt.py | 21 +++++-------- homeassistant/components/lock/nuki.py | 4 +-- homeassistant/components/lock/wink.py | 4 +-- homeassistant/components/lock/zwave.py | 8 ++--- homeassistant/components/remote/__init__.py | 6 ++-- homeassistant/components/remote/apple_tv.py | 17 ++++------ homeassistant/components/remote/harmony.py | 4 +-- .../components/remote/xiaomi_miio.py | 22 ++++++------- .../components/scene/homeassistant.py | 11 +++---- .../scene/hunterdouglas_powerview.py | 10 +++--- homeassistant/components/scene/lifx_cloud.py | 14 ++++----- .../components/scene/lutron_caseta.py | 9 ++---- homeassistant/components/scene/wink.py | 4 +-- .../components/telegram_bot/__init__.py | 23 ++++++-------- .../components/telegram_bot/broadcast.py | 6 ++-- .../components/telegram_bot/polling.py | 4 +-- .../components/telegram_bot/webhooks.py | 13 +++----- homeassistant/helpers/entity.py | 15 ++++----- 28 files changed, 112 insertions(+), 195 deletions(-) diff --git a/homeassistant/components/light/ads.py b/homeassistant/components/light/ads.py index 65569f6b2d5..0d97efa16a2 100644 --- a/homeassistant/components/light/ads.py +++ b/homeassistant/components/light/ads.py @@ -5,7 +5,6 @@ For more details about this platform, please refer to the documentation. https://home-assistant.io/components/light.ads/ """ -import asyncio import logging import voluptuous as vol from homeassistant.components.light import Light, ATTR_BRIGHTNESS, \ @@ -50,8 +49,7 @@ class AdsLight(Light): self.ads_var_enable = ads_var_enable self.ads_var_brightness = ads_var_brightness - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Register device notification.""" def update_on_state(name, value): """Handle device notifications for state.""" diff --git a/homeassistant/components/light/insteon.py b/homeassistant/components/light/insteon.py index 82f455c821e..4829ce631a6 100644 --- a/homeassistant/components/light/insteon.py +++ b/homeassistant/components/light/insteon.py @@ -4,7 +4,6 @@ Support for Insteon lights via PowerLinc Modem. For more details about this component, please refer to the documentation at https://home-assistant.io/components/light.insteon/ """ -import asyncio import logging from homeassistant.components.insteon import InsteonEntity @@ -18,9 +17,8 @@ DEPENDENCIES = ['insteon'] MAX_BRIGHTNESS = 255 -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the Insteon component.""" insteon_modem = hass.data['insteon'].get('modem') @@ -55,8 +53,7 @@ class InsteonDimmerDevice(InsteonEntity, Light): """Flag supported features.""" return SUPPORT_BRIGHTNESS - @asyncio.coroutine - def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): """Turn device on.""" if ATTR_BRIGHTNESS in kwargs: brightness = int(kwargs[ATTR_BRIGHTNESS]) @@ -64,7 +61,6 @@ class InsteonDimmerDevice(InsteonEntity, Light): else: self._insteon_device_state.on() - @asyncio.coroutine - def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs): """Turn device off.""" self._insteon_device_state.off() diff --git a/homeassistant/components/light/limitlessled.py b/homeassistant/components/light/limitlessled.py index 9400932802a..a5aeabba84d 100644 --- a/homeassistant/components/light/limitlessled.py +++ b/homeassistant/components/light/limitlessled.py @@ -4,7 +4,6 @@ Support for LimitlessLED bulbs. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/light.limitlessled/ """ -import asyncio import logging import voluptuous as vol @@ -188,10 +187,9 @@ class LimitlessLEDGroup(Light): self._color = None self._effect = None - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Handle entity about to be added to hass event.""" - last_state = yield from async_get_last_state(self.hass, self.entity_id) + last_state = await async_get_last_state(self.hass, self.entity_id) if last_state: self._is_on = (last_state.state == STATE_ON) self._brightness = last_state.attributes.get('brightness') diff --git a/homeassistant/components/light/lutron_caseta.py b/homeassistant/components/light/lutron_caseta.py index f345748683b..21360e71c42 100644 --- a/homeassistant/components/light/lutron_caseta.py +++ b/homeassistant/components/light/lutron_caseta.py @@ -4,7 +4,6 @@ Support for Lutron Caseta lights. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/light.lutron_caseta/ """ -import asyncio import logging from homeassistant.components.light import ( @@ -19,9 +18,8 @@ _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['lutron_caseta'] -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the Lutron Caseta lights.""" devs = [] bridge = hass.data[LUTRON_CASETA_SMARTBRIDGE] @@ -46,15 +44,13 @@ class LutronCasetaLight(LutronCasetaDevice, Light): """Return the brightness of the light.""" return to_hass_level(self._state["current_state"]) - @asyncio.coroutine - def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): """Turn the light on.""" brightness = kwargs.get(ATTR_BRIGHTNESS, 255) self._smartbridge.set_value(self._device_id, to_lutron_level(brightness)) - @asyncio.coroutine - def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs): """Turn the light off.""" self._smartbridge.set_value(self._device_id, 0) @@ -63,8 +59,7 @@ class LutronCasetaLight(LutronCasetaDevice, Light): """Return true if device is on.""" return self._state["current_state"] > 0 - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Call when forcing a refresh of the device.""" self._state = self._smartbridge.get_device_by_id(self._device_id) _LOGGER.debug(self._state) diff --git a/homeassistant/components/light/rflink.py b/homeassistant/components/light/rflink.py index b410fdceff7..d9f9dd589ec 100644 --- a/homeassistant/components/light/rflink.py +++ b/homeassistant/components/light/rflink.py @@ -4,7 +4,6 @@ Support for Rflink lights. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/light.rflink/ """ -import asyncio import logging from homeassistant.components.light import ( @@ -155,14 +154,12 @@ def devices_from_config(domain_config, hass=None): return devices -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the Rflink light platform.""" async_add_entities(devices_from_config(config, hass)) - @asyncio.coroutine - def add_new_device(event): + async def add_new_device(event): """Check if device is known, otherwise add to list of known devices.""" device_id = event[EVENT_KEY_ID] @@ -195,15 +192,14 @@ class DimmableRflinkLight(SwitchableRflinkDevice, Light): _brightness = 255 - @asyncio.coroutine - def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): """Turn the device on.""" if ATTR_BRIGHTNESS in kwargs: # rflink only support 16 brightness levels self._brightness = int(kwargs[ATTR_BRIGHTNESS] / 17) * 17 # Turn on light at the requested dim level - yield from self._async_handle_command('dim', self._brightness) + await self._async_handle_command('dim', self._brightness) @property def brightness(self): @@ -233,8 +229,7 @@ class HybridRflinkLight(SwitchableRflinkDevice, Light): _brightness = 255 - @asyncio.coroutine - def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): """Turn the device on and set dim level.""" if ATTR_BRIGHTNESS in kwargs: # rflink only support 16 brightness levels @@ -242,12 +237,12 @@ class HybridRflinkLight(SwitchableRflinkDevice, Light): # if receiver supports dimming this will turn on the light # at the requested dim level - yield from self._async_handle_command('dim', self._brightness) + await self._async_handle_command('dim', self._brightness) # if the receiving device does not support dimlevel this # will ensure it is turned on when full brightness is set if self._brightness == 255: - yield from self._async_handle_command('turn_on') + await self._async_handle_command('turn_on') @property def brightness(self): @@ -284,12 +279,10 @@ class ToggleRflinkLight(SwitchableRflinkDevice, Light): # if the state is true, it gets set as false self._state = self._state in [STATE_UNKNOWN, False] - @asyncio.coroutine - def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): """Turn the device on.""" - yield from self._async_handle_command('toggle') + await self._async_handle_command('toggle') - @asyncio.coroutine - def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs): """Turn the device off.""" - yield from self._async_handle_command('toggle') + await self._async_handle_command('toggle') diff --git a/homeassistant/components/light/template.py b/homeassistant/components/light/template.py index 9be6eb99acc..f90147f7169 100644 --- a/homeassistant/components/light/template.py +++ b/homeassistant/components/light/template.py @@ -5,7 +5,6 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/light.template/ """ import logging -import asyncio import voluptuous as vol @@ -49,9 +48,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the Template Lights.""" lights = [] @@ -182,8 +180,7 @@ class LightTemplate(Light): """Return the entity picture to use in the frontend, if any.""" return self._entity_picture - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Register callbacks.""" @callback def template_light_state_listener(entity, old_state, new_state): @@ -203,8 +200,7 @@ class LightTemplate(Light): self.hass.bus.async_listen_once( EVENT_HOMEASSISTANT_START, template_light_startup) - @asyncio.coroutine - def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): """Turn the light on.""" optimistic_set = False # set optimistic states @@ -222,21 +218,19 @@ class LightTemplate(Light): self.hass.async_add_job(self._level_script.async_run( {"brightness": kwargs[ATTR_BRIGHTNESS]})) else: - yield from self._on_script.async_run() + await self._on_script.async_run() if optimistic_set: self.async_schedule_update_ha_state() - @asyncio.coroutine - def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs): """Turn the light off.""" - yield from self._off_script.async_run() + await self._off_script.async_run() if self._template is None: self._state = False self.async_schedule_update_ha_state() - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Update the state from the template.""" if self._template is not None: try: diff --git a/homeassistant/components/light/wemo.py b/homeassistant/components/light/wemo.py index 72279fbe1a4..55a4836e148 100644 --- a/homeassistant/components/light/wemo.py +++ b/homeassistant/components/light/wemo.py @@ -4,7 +4,6 @@ Support for Belkin WeMo lights. For more details about this component, please refer to the documentation at https://home-assistant.io/components/light.wemo/ """ -import asyncio import logging from datetime import timedelta import requests @@ -160,13 +159,12 @@ class WemoDimmer(Light): self._brightness = None self._state = None - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Register update callback.""" wemo = self.hass.components.wemo # The register method uses a threading condition, so call via executor. - # and yield from to wait until the task is done. - yield from self.hass.async_add_job( + # and await to wait until the task is done. + await self.hass.async_add_job( wemo.SUBSCRIPTION_REGISTRY.register, self.wemo) # The on method just appends to a defaultdict list. wemo.SUBSCRIPTION_REGISTRY.on(self.wemo, None, self._update_callback) diff --git a/homeassistant/components/light/wink.py b/homeassistant/components/light/wink.py index ee8c2aca8b5..96c8f20679e 100644 --- a/homeassistant/components/light/wink.py +++ b/homeassistant/components/light/wink.py @@ -4,7 +4,6 @@ Support for Wink lights. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/light.wink/ """ -import asyncio from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, SUPPORT_BRIGHTNESS, @@ -34,8 +33,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class WinkLight(WinkDevice, Light): """Representation of a Wink light.""" - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Call when entity is added to hass.""" self.hass.data[DOMAIN]['entities']['light'].append(self) diff --git a/homeassistant/components/lock/__init__.py b/homeassistant/components/lock/__init__.py index 5218ea49c80..e9904f0163a 100644 --- a/homeassistant/components/lock/__init__.py +++ b/homeassistant/components/lock/__init__.py @@ -4,7 +4,6 @@ Component to interface with various locks that can be controlled remotely. For more details about this component, please refer to the documentation at https://home-assistant.io/components/lock/ """ -import asyncio from datetime import timedelta import functools as ft import logging @@ -57,13 +56,12 @@ def is_locked(hass, entity_id=None): return hass.states.is_state(entity_id, STATE_LOCKED) -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Track states and offer events for locks.""" component = EntityComponent( _LOGGER, DOMAIN, hass, SCAN_INTERVAL, GROUP_NAME_ALL_LOCKS) - yield from component.async_setup(config) + await component.async_setup(config) component.async_register_entity_service( SERVICE_UNLOCK, LOCK_SERVICE_SCHEMA, diff --git a/homeassistant/components/lock/bmw_connected_drive.py b/homeassistant/components/lock/bmw_connected_drive.py index c84df54cfba..b13665610d8 100644 --- a/homeassistant/components/lock/bmw_connected_drive.py +++ b/homeassistant/components/lock/bmw_connected_drive.py @@ -4,7 +4,6 @@ Support for BMW cars with BMW ConnectedDrive. For more details about this component, please refer to the documentation at https://home-assistant.io/components/lock.bmw_connected_drive/ """ -import asyncio import logging from homeassistant.components.bmw_connected_drive import DOMAIN as BMW_DOMAIN @@ -111,8 +110,7 @@ class BMWLock(LockDevice): """Schedule a state update.""" self.schedule_update_ha_state(True) - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Add callback after being added to hass. Show latest data after startup. diff --git a/homeassistant/components/lock/mqtt.py b/homeassistant/components/lock/mqtt.py index b9b094b615b..ee43eb942c4 100644 --- a/homeassistant/components/lock/mqtt.py +++ b/homeassistant/components/lock/mqtt.py @@ -4,7 +4,6 @@ Support for MQTT locks. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/lock.mqtt/ """ -import asyncio import logging import voluptuous as vol @@ -41,9 +40,8 @@ PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({ }).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the MQTT lock.""" if discovery_info is not None: config = PLATFORM_SCHEMA(discovery_info) @@ -96,11 +94,10 @@ class MqttLock(MqttAvailability, MqttDiscoveryUpdate, LockDevice): self._template = value_template self._discovery_hash = discovery_hash - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Subscribe to MQTT events.""" - yield from MqttAvailability.async_added_to_hass(self) - yield from MqttDiscoveryUpdate.async_added_to_hass(self) + await MqttAvailability.async_added_to_hass(self) + await MqttDiscoveryUpdate.async_added_to_hass(self) @callback def message_received(topic, payload, qos): @@ -119,7 +116,7 @@ class MqttLock(MqttAvailability, MqttDiscoveryUpdate, LockDevice): # Force into optimistic mode. self._optimistic = True else: - yield from mqtt.async_subscribe( + await mqtt.async_subscribe( self.hass, self._state_topic, message_received, self._qos) @property @@ -142,8 +139,7 @@ class MqttLock(MqttAvailability, MqttDiscoveryUpdate, LockDevice): """Return true if we do optimistic updates.""" return self._optimistic - @asyncio.coroutine - def async_lock(self, **kwargs): + async def async_lock(self, **kwargs): """Lock the device. This method is a coroutine. @@ -156,8 +152,7 @@ class MqttLock(MqttAvailability, MqttDiscoveryUpdate, LockDevice): self._state = True self.async_schedule_update_ha_state() - @asyncio.coroutine - def async_unlock(self, **kwargs): + async def async_unlock(self, **kwargs): """Unlock the device. This method is a coroutine. diff --git a/homeassistant/components/lock/nuki.py b/homeassistant/components/lock/nuki.py index 6cf58dda04c..689ec31fc7c 100644 --- a/homeassistant/components/lock/nuki.py +++ b/homeassistant/components/lock/nuki.py @@ -4,7 +4,6 @@ Nuki.io lock platform. For more details about this platform, please refer to the documentation https://home-assistant.io/components/lock.nuki/ """ -import asyncio from datetime import timedelta import logging @@ -92,8 +91,7 @@ class NukiLock(LockDevice): self._name = nuki_lock.name self._battery_critical = nuki_lock.battery_critical - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Call when entity is added to hass.""" if NUKI_DATA not in self.hass.data: self.hass.data[NUKI_DATA] = {} diff --git a/homeassistant/components/lock/wink.py b/homeassistant/components/lock/wink.py index 03de8fc5919..68cc7a79ae6 100644 --- a/homeassistant/components/lock/wink.py +++ b/homeassistant/components/lock/wink.py @@ -4,7 +4,6 @@ Support for Wink locks. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/lock.wink/ """ -import asyncio import logging import voluptuous as vol @@ -131,8 +130,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class WinkLockDevice(WinkDevice, LockDevice): """Representation of a Wink lock.""" - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Call when entity is added to hass.""" self.hass.data[DOMAIN]['entities']['lock'].append(self) diff --git a/homeassistant/components/lock/zwave.py b/homeassistant/components/lock/zwave.py index 5ee88f053b5..4a2b71f0b48 100644 --- a/homeassistant/components/lock/zwave.py +++ b/homeassistant/components/lock/zwave.py @@ -4,7 +4,6 @@ Z-Wave platform that handles simple door locks. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/lock.zwave/ """ -import asyncio import logging import voluptuous as vol @@ -119,11 +118,10 @@ CLEAR_USERCODE_SCHEMA = vol.Schema({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the Z-Wave Lock platform.""" - yield from zwave.async_setup_platform( + await zwave.async_setup_platform( hass, config, async_add_entities, discovery_info) network = hass.data[zwave.const.DATA_NETWORK] diff --git a/homeassistant/components/remote/__init__.py b/homeassistant/components/remote/__init__.py index 3fd2a5d4c44..4fc491e57e8 100644 --- a/homeassistant/components/remote/__init__.py +++ b/homeassistant/components/remote/__init__.py @@ -4,7 +4,6 @@ Component to interface with universal remote control devices. For more details about this component, please refer to the documentation at https://home-assistant.io/components/remote/ """ -import asyncio from datetime import timedelta import functools as ft import logging @@ -70,12 +69,11 @@ def is_on(hass, entity_id=None): return hass.states.is_state(entity_id, STATE_ON) -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Track states and offer events for remotes.""" component = EntityComponent( _LOGGER, DOMAIN, hass, SCAN_INTERVAL, GROUP_NAME_ALL_REMOTES) - yield from component.async_setup(config) + await component.async_setup(config) component.async_register_entity_service( SERVICE_TURN_OFF, REMOTE_SERVICE_ACTIVITY_SCHEMA, diff --git a/homeassistant/components/remote/apple_tv.py b/homeassistant/components/remote/apple_tv.py index d8eac11372c..72696143bfe 100644 --- a/homeassistant/components/remote/apple_tv.py +++ b/homeassistant/components/remote/apple_tv.py @@ -4,7 +4,6 @@ Remote control support for Apple TV. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/remote.apple_tv/ """ -import asyncio from homeassistant.components.apple_tv import ( ATTR_ATV, ATTR_POWER, DATA_APPLE_TV) @@ -15,9 +14,8 @@ from homeassistant.const import (CONF_NAME, CONF_HOST) DEPENDENCIES = ['apple_tv'] -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the Apple TV remote platform.""" if not discovery_info: return @@ -59,16 +57,14 @@ class AppleTVRemote(remote.RemoteDevice): """No polling needed for Apple TV.""" return False - @asyncio.coroutine - def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): """Turn the device on. This method is a coroutine. """ self._power.set_power_on(True) - @asyncio.coroutine - def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs): """Turn the device off. This method is a coroutine. @@ -81,12 +77,11 @@ class AppleTVRemote(remote.RemoteDevice): This method must be run in the event loop and returns a coroutine. """ # Send commands in specified order but schedule only one coroutine - @asyncio.coroutine - def _send_commands(): + async def _send_commands(): for single_command in command: if not hasattr(self._atv.remote_control, single_command): continue - yield from getattr(self._atv.remote_control, single_command)() + await getattr(self._atv.remote_control, single_command)() return _send_commands() diff --git a/homeassistant/components/remote/harmony.py b/homeassistant/components/remote/harmony.py index 5b7d0d1df78..14008d49760 100644 --- a/homeassistant/components/remote/harmony.py +++ b/homeassistant/components/remote/harmony.py @@ -4,7 +4,6 @@ Support for Harmony Hub devices. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/remote.harmony/ """ -import asyncio import logging import time @@ -152,8 +151,7 @@ class HarmonyRemote(remote.RemoteDevice): pyharmony.ha_write_config_file(self._config, self._config_path) self._delay_secs = delay_secs - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Complete the initialization.""" self.hass.bus.async_listen_once( EVENT_HOMEASSISTANT_STOP, diff --git a/homeassistant/components/remote/xiaomi_miio.py b/homeassistant/components/remote/xiaomi_miio.py index 723f575ba34..7cd588683de 100644 --- a/homeassistant/components/remote/xiaomi_miio.py +++ b/homeassistant/components/remote/xiaomi_miio.py @@ -61,9 +61,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }, extra=vol.ALLOW_EXTRA) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the Xiaomi IR Remote (Chuangmi IR) platform.""" from miio import ChuangmiIr, DeviceException @@ -109,8 +108,7 @@ def async_setup_platform(hass, config, async_add_entities, async_add_entities([xiaomi_miio_remote]) - @asyncio.coroutine - def async_service_handler(service): + async def async_service_handler(service): """Handle a learn command.""" if service.service != SERVICE_LEARN: _LOGGER.error("We should not handle service: %s", service.service) @@ -130,14 +128,14 @@ def async_setup_platform(hass, config, async_add_entities, slot = service.data.get(CONF_SLOT, entity.slot) - yield from hass.async_add_job(device.learn, slot) + await hass.async_add_job(device.learn, slot) timeout = service.data.get(CONF_TIMEOUT, entity.timeout) _LOGGER.info("Press the key you want Home Assistant to learn") start_time = utcnow() while (utcnow() - start_time) < timedelta(seconds=timeout): - message = yield from hass.async_add_job( + message = await hass.async_add_job( device.read, slot) _LOGGER.debug("Message received from device: '%s'", message) @@ -150,9 +148,9 @@ def async_setup_platform(hass, config, async_add_entities, if ('error' in message and message['error']['message'] == "learn timeout"): - yield from hass.async_add_job(device.learn, slot) + await hass.async_add_job(device.learn, slot) - yield from asyncio.sleep(1, loop=hass.loop) + await asyncio.sleep(1, loop=hass.loop) _LOGGER.error("Timeout. No infrared command captured") hass.components.persistent_notification.async_create( @@ -230,14 +228,12 @@ class XiaomiMiioRemote(RemoteDevice): return {'hidden': 'true'} return - @asyncio.coroutine - def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): """Turn the device on.""" _LOGGER.error("Device does not support turn_on, " "please use 'remote.send_command' to send commands.") - @asyncio.coroutine - def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs): """Turn the device off.""" _LOGGER.error("Device does not support turn_off, " "please use 'remote.send_command' to send commands.") diff --git a/homeassistant/components/scene/homeassistant.py b/homeassistant/components/scene/homeassistant.py index 7e1d670ca69..5812512ccef 100644 --- a/homeassistant/components/scene/homeassistant.py +++ b/homeassistant/components/scene/homeassistant.py @@ -4,7 +4,6 @@ Allow users to set and activate scenes. For more details about this component, please refer to the documentation at https://home-assistant.io/components/scene/ """ -import asyncio from collections import namedtuple import voluptuous as vol @@ -35,9 +34,8 @@ PLATFORM_SCHEMA = vol.Schema({ SCENECONFIG = namedtuple('SceneConfig', [CONF_NAME, STATES]) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up home assistant scene entries.""" scene_config = config.get(STATES) @@ -97,8 +95,7 @@ class HomeAssistantScene(Scene): ATTR_ENTITY_ID: list(self.scene_config.states.keys()), } - @asyncio.coroutine - def async_activate(self): + async def async_activate(self): """Activate scene. Try to get entities into requested state.""" - yield from async_reproduce_state( + await async_reproduce_state( self.hass, self.scene_config.states.values(), True) diff --git a/homeassistant/components/scene/hunterdouglas_powerview.py b/homeassistant/components/scene/hunterdouglas_powerview.py index 40534b68635..8c1ffa17e90 100644 --- a/homeassistant/components/scene/hunterdouglas_powerview.py +++ b/homeassistant/components/scene/hunterdouglas_powerview.py @@ -4,7 +4,6 @@ Support for Powerview scenes from a Powerview hub. For more details about this component, please refer to the documentation at https://home-assistant.io/components/scene.hunterdouglas_powerview/ """ -import asyncio import logging import voluptuous as vol @@ -36,9 +35,8 @@ ROOM_ID_IN_SCENE = 'roomId' STATE_ATTRIBUTE_ROOM_NAME = 'roomName' -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +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.scenes import Scenes @@ -48,9 +46,9 @@ def async_setup_platform(hass, config, async_add_entities, hub_address = config.get(HUB_ADDRESS) websession = async_get_clientsession(hass) - _scenes = yield from Scenes( + _scenes = await Scenes( hub_address, hass.loop, websession).get_resources() - _rooms = yield from Rooms( + _rooms = await Rooms( hub_address, hass.loop, websession).get_resources() if not _scenes or not _rooms: diff --git a/homeassistant/components/scene/lifx_cloud.py b/homeassistant/components/scene/lifx_cloud.py index 3169acb3a31..c1dda86343d 100644 --- a/homeassistant/components/scene/lifx_cloud.py +++ b/homeassistant/components/scene/lifx_cloud.py @@ -29,9 +29,8 @@ PLATFORM_SCHEMA = vol.Schema({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the scenes stored in the LIFX Cloud.""" token = config.get(CONF_TOKEN) timeout = config.get(CONF_TIMEOUT) @@ -45,7 +44,7 @@ def async_setup_platform(hass, config, async_add_entities, try: httpsession = async_get_clientsession(hass) with async_timeout.timeout(timeout, loop=hass.loop): - scenes_resp = yield from httpsession.get(url, headers=headers) + scenes_resp = await httpsession.get(url, headers=headers) except (asyncio.TimeoutError, aiohttp.ClientError): _LOGGER.exception("Error on %s", url) @@ -53,7 +52,7 @@ def async_setup_platform(hass, config, async_add_entities, status = scenes_resp.status if status == 200: - data = yield from scenes_resp.json() + data = await scenes_resp.json() devices = [] for scene in data: devices.append(LifxCloudScene(hass, headers, timeout, scene)) @@ -83,15 +82,14 @@ class LifxCloudScene(Scene): """Return the name of the scene.""" return self._name - @asyncio.coroutine - def async_activate(self): + async def async_activate(self): """Activate the scene.""" url = LIFX_API_URL.format('scenes/scene_id:%s/activate' % self._uuid) try: httpsession = async_get_clientsession(self.hass) with async_timeout.timeout(self._timeout, loop=self.hass.loop): - yield from httpsession.put(url, headers=self._headers) + await httpsession.put(url, headers=self._headers) except (asyncio.TimeoutError, aiohttp.ClientError): _LOGGER.exception("Error on %s", url) diff --git a/homeassistant/components/scene/lutron_caseta.py b/homeassistant/components/scene/lutron_caseta.py index 0f9173663a9..0ef974e2778 100644 --- a/homeassistant/components/scene/lutron_caseta.py +++ b/homeassistant/components/scene/lutron_caseta.py @@ -4,7 +4,6 @@ Support for Lutron Caseta scenes. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/scene.lutron_caseta/ """ -import asyncio import logging from homeassistant.components.lutron_caseta import LUTRON_CASETA_SMARTBRIDGE @@ -15,9 +14,8 @@ _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['lutron_caseta'] -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the Lutron Caseta lights.""" devs = [] bridge = hass.data[LUTRON_CASETA_SMARTBRIDGE] @@ -43,7 +41,6 @@ class LutronCasetaScene(Scene): """Return the name of the scene.""" return self._scene_name - @asyncio.coroutine - def async_activate(self): + async def async_activate(self): """Activate the scene.""" self._bridge.activate_scene(self._scene_id) diff --git a/homeassistant/components/scene/wink.py b/homeassistant/components/scene/wink.py index 62da668694b..35db96c3b8b 100644 --- a/homeassistant/components/scene/wink.py +++ b/homeassistant/components/scene/wink.py @@ -4,7 +4,6 @@ Support for Wink scenes. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/scene.wink/ """ -import asyncio import logging from homeassistant.components.scene import Scene @@ -33,8 +32,7 @@ class WinkScene(WinkDevice, Scene): super().__init__(wink, hass) hass.data[DOMAIN]['entities']['scene'].append(self) - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Call when entity is added to hass.""" self.hass.data[DOMAIN]['entities']['scene'].append(self) diff --git a/homeassistant/components/telegram_bot/__init__.py b/homeassistant/components/telegram_bot/__init__.py index 75d9b611591..40724a1ee86 100644 --- a/homeassistant/components/telegram_bot/__init__.py +++ b/homeassistant/components/telegram_bot/__init__.py @@ -4,7 +4,6 @@ Component to send and receive Telegram messages. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/telegram_bot/ """ -import asyncio import io from functools import partial import logging @@ -210,8 +209,7 @@ def load_data(hass, url=None, filepath=None, username=None, password=None, return None -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Set up the Telegram bot component.""" if not config[DOMAIN]: return False @@ -220,7 +218,7 @@ def async_setup(hass, config): p_type = p_config.get(CONF_PLATFORM) - platform = yield from async_prepare_setup_platform( + platform = await async_prepare_setup_platform( hass, config, DOMAIN, p_type) if platform is None: @@ -228,7 +226,7 @@ def async_setup(hass, config): _LOGGER.info("Setting up %s.%s", DOMAIN, p_type) try: - receiver_service = yield from \ + receiver_service = await \ platform.async_setup_platform(hass, p_config) if receiver_service is False: _LOGGER.error( @@ -247,8 +245,7 @@ def async_setup(hass, config): p_config.get(ATTR_PARSER) ) - @asyncio.coroutine - def async_send_telegram_message(service): + async def async_send_telegram_message(service): """Handle sending Telegram Bot message service calls.""" def _render_template_attr(data, attribute): attribute_templ = data.get(attribute) @@ -274,23 +271,23 @@ def async_setup(hass, config): _LOGGER.debug("New telegram message %s: %s", msgtype, kwargs) if msgtype == SERVICE_SEND_MESSAGE: - yield from hass.async_add_job( + await hass.async_add_job( partial(notify_service.send_message, **kwargs)) elif msgtype in [SERVICE_SEND_PHOTO, SERVICE_SEND_STICKER, SERVICE_SEND_VIDEO, SERVICE_SEND_DOCUMENT]: - yield from hass.async_add_job( + await hass.async_add_job( partial(notify_service.send_file, msgtype, **kwargs)) elif msgtype == SERVICE_SEND_LOCATION: - yield from hass.async_add_job( + await hass.async_add_job( partial(notify_service.send_location, **kwargs)) elif msgtype == SERVICE_ANSWER_CALLBACK_QUERY: - yield from hass.async_add_job( + await hass.async_add_job( partial(notify_service.answer_callback_query, **kwargs)) elif msgtype == SERVICE_DELETE_MESSAGE: - yield from hass.async_add_job( + await hass.async_add_job( partial(notify_service.delete_message, **kwargs)) else: - yield from hass.async_add_job( + await hass.async_add_job( partial(notify_service.edit_message, msgtype, **kwargs)) # Register notification services diff --git a/homeassistant/components/telegram_bot/broadcast.py b/homeassistant/components/telegram_bot/broadcast.py index 7e157fdb0c7..7cfcc272a33 100644 --- a/homeassistant/components/telegram_bot/broadcast.py +++ b/homeassistant/components/telegram_bot/broadcast.py @@ -4,7 +4,6 @@ Telegram bot implementation to send messages only. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/telegram_bot.broadcast/ """ -import asyncio import logging from homeassistant.components.telegram_bot import ( @@ -16,14 +15,13 @@ _LOGGER = logging.getLogger(__name__) PLATFORM_SCHEMA = TELEGRAM_PLATFORM_SCHEMA -@asyncio.coroutine -def async_setup_platform(hass, config): +async def async_setup_platform(hass, config): """Set up the Telegram broadcast platform.""" # Check the API key works bot = initialize_bot(config) - bot_config = yield from hass.async_add_job(bot.getMe) + bot_config = await hass.async_add_job(bot.getMe) _LOGGER.debug("Telegram broadcast platform setup with bot %s", bot_config['username']) return True diff --git a/homeassistant/components/telegram_bot/polling.py b/homeassistant/components/telegram_bot/polling.py index 0d4eddffd32..d1dea051985 100644 --- a/homeassistant/components/telegram_bot/polling.py +++ b/homeassistant/components/telegram_bot/polling.py @@ -4,7 +4,6 @@ Telegram bot polling implementation. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/telegram_bot.polling/ """ -import asyncio import logging from homeassistant.components.telegram_bot import ( @@ -20,8 +19,7 @@ _LOGGER = logging.getLogger(__name__) PLATFORM_SCHEMA = TELEGRAM_PLATFORM_SCHEMA -@asyncio.coroutine -def async_setup_platform(hass, config): +async def async_setup_platform(hass, config): """Set up the Telegram polling platform.""" bot = initialize_bot(config) pol = TelegramPoll(bot, hass, config[CONF_ALLOWED_CHAT_IDS]) diff --git a/homeassistant/components/telegram_bot/webhooks.py b/homeassistant/components/telegram_bot/webhooks.py index b7dd7ab8269..72e8c557fe5 100644 --- a/homeassistant/components/telegram_bot/webhooks.py +++ b/homeassistant/components/telegram_bot/webhooks.py @@ -4,7 +4,6 @@ Allows utilizing telegram webhooks. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/telegram_bot.webhooks/ """ -import asyncio import datetime as dt from ipaddress import ip_network import logging @@ -47,13 +46,12 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform(hass, config): +async def async_setup_platform(hass, config): """Set up the Telegram webhooks platform.""" import telegram bot = initialize_bot(config) - current_status = yield from hass.async_add_job(bot.getWebhookInfo) + current_status = await hass.async_add_job(bot.getWebhookInfo) base_url = config.get(CONF_URL, hass.config.api.base_url) # Some logging of Bot current status: @@ -81,7 +79,7 @@ def async_setup_platform(hass, config): retry_num) if current_status and current_status['url'] != handler_url: - result = yield from hass.async_add_job(_try_to_set_webhook) + result = await hass.async_add_job(_try_to_set_webhook) if result: _LOGGER.info("Set new telegram webhook %s", handler_url) else: @@ -108,8 +106,7 @@ class BotPushReceiver(HomeAssistantView, BaseTelegramBotEntity): BaseTelegramBotEntity.__init__(self, hass, allowed_chat_ids) self.trusted_networks = trusted_networks - @asyncio.coroutine - def post(self, request): + async def post(self, request): """Accept the POST from telegram.""" real_ip = request[KEY_REAL_IP] if not any(real_ip in net for net in self.trusted_networks): @@ -117,7 +114,7 @@ class BotPushReceiver(HomeAssistantView, BaseTelegramBotEntity): return self.json_message('Access denied', HTTP_UNAUTHORIZED) try: - data = yield from request.json() + data = await request.json() except ValueError: return self.json_message('Invalid JSON', HTTP_BAD_REQUEST) diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index e48af6a3365..60fd661a765 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -1,5 +1,4 @@ """An abstract class for entities.""" -import asyncio from datetime import timedelta import logging import functools as ft @@ -202,8 +201,7 @@ class Entity: self._context = context self._context_set = dt_util.utcnow() - @asyncio.coroutine - def async_update_ha_state(self, force_refresh=False): + async def async_update_ha_state(self, force_refresh=False): """Update Home Assistant with current state of entity. If force_refresh == True will update entity before setting state. @@ -220,7 +218,7 @@ class Entity: # update entity data if force_refresh: try: - yield from self.async_device_update() + await self.async_device_update() except Exception: # pylint: disable=broad-except _LOGGER.exception("Update for %s fails", self.entity_id) return @@ -323,8 +321,7 @@ class Entity: """Schedule an update ha state change task.""" self.hass.async_add_job(self.async_update_ha_state(force_refresh)) - @asyncio.coroutine - def async_device_update(self, warning=True): + async def async_device_update(self, warning=True): """Process 'update' or 'async_update' from entity. This method is a coroutine. @@ -335,7 +332,7 @@ class Entity: # Process update sequential if self.parallel_updates: - yield from self.parallel_updates.acquire() + await self.parallel_updates.acquire() if warning: update_warn = self.hass.loop.call_later( @@ -347,9 +344,9 @@ class Entity: try: # pylint: disable=no-member if hasattr(self, 'async_update'): - yield from self.async_update() + await self.async_update() elif hasattr(self, 'update'): - yield from self.hass.async_add_job(self.update) + await self.hass.async_add_job(self.update) finally: self._update_staged = False if warning: From 134eeecd65641cd0a24aa64039d792eb14e12605 Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Mon, 1 Oct 2018 08:58:21 +0200 Subject: [PATCH 123/247] Async syntax 4/8 (#17018) * Async syntax 4, media_player & notify * Pylint fixes --- .../components/media_player/anthemav.py | 23 +-- .../components/media_player/apple_tv.py | 23 +-- .../components/media_player/clementine.py | 4 +- homeassistant/components/media_player/emby.py | 14 +- .../media_player/frontier_silicon.py | 98 +++++------ homeassistant/components/media_player/kodi.py | 164 ++++++++---------- .../components/media_player/liveboxplaytv.py | 13 +- .../components/media_player/russound_rio.py | 15 +- .../components/media_player/snapcast.py | 48 ++--- .../components/media_player/sonos.py | 4 +- .../components/media_player/squeezebox.py | 33 ++-- .../components/media_player/volumio.py | 19 +- homeassistant/components/notify/__init__.py | 24 ++- homeassistant/components/notify/discord.py | 17 +- homeassistant/components/notify/group.py | 8 +- homeassistant/components/notify/kodi.py | 9 +- homeassistant/components/notify/prowl.py | 10 +- 17 files changed, 212 insertions(+), 314 deletions(-) diff --git a/homeassistant/components/media_player/anthemav.py b/homeassistant/components/media_player/anthemav.py index 33b6e28a890..c1415f985e8 100644 --- a/homeassistant/components/media_player/anthemav.py +++ b/homeassistant/components/media_player/anthemav.py @@ -4,7 +4,6 @@ Support for Anthem Network Receivers and Processors. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/media_player.anthemav/ """ -import asyncio import logging import voluptuous as vol @@ -35,9 +34,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up our socket to the AVR.""" import anthemav @@ -53,7 +51,7 @@ def async_setup_platform(hass, config, async_add_entities, _LOGGER.info("Received update callback from AVR: %s", message) hass.async_add_job(device.async_update_ha_state()) - avr = yield from anthemav.Connection.create( + avr = await anthemav.Connection.create( host=host, port=port, loop=hass.loop, update_callback=async_anthemav_update_callback) @@ -136,28 +134,23 @@ class AnthemAVR(MediaPlayerDevice): """Return all active, configured inputs.""" return self._lookup('input_list', ["Unknown"]) - @asyncio.coroutine - def async_select_source(self, source): + async def async_select_source(self, source): """Change AVR to the designated source (by name).""" self._update_avr('input_name', source) - @asyncio.coroutine - def async_turn_off(self): + async def async_turn_off(self): """Turn AVR power off.""" self._update_avr('power', False) - @asyncio.coroutine - def async_turn_on(self): + async def async_turn_on(self): """Turn AVR power on.""" self._update_avr('power', True) - @asyncio.coroutine - def async_set_volume_level(self, volume): + async def async_set_volume_level(self, volume): """Set AVR volume (0 to 1).""" self._update_avr('volume_as_percentage', volume) - @asyncio.coroutine - def async_mute_volume(self, mute): + async def async_mute_volume(self, mute): """Engage AVR mute.""" self._update_avr('mute', mute) diff --git a/homeassistant/components/media_player/apple_tv.py b/homeassistant/components/media_player/apple_tv.py index 399e59ae9f5..bff8834639d 100644 --- a/homeassistant/components/media_player/apple_tv.py +++ b/homeassistant/components/media_player/apple_tv.py @@ -4,7 +4,6 @@ Support for Apple TV. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/media_player.apple_tv/ """ -import asyncio import logging from homeassistant.components.apple_tv import ( @@ -29,8 +28,7 @@ SUPPORT_APPLE_TV = SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_PLAY_MEDIA | \ SUPPORT_STOP | SUPPORT_NEXT_TRACK | SUPPORT_PREVIOUS_TRACK -@asyncio.coroutine -def async_setup_platform( +async def async_setup_platform( hass, config, async_add_entities, discovery_info=None): """Set up the Apple TV platform.""" if not discovery_info: @@ -71,8 +69,7 @@ class AppleTvDevice(MediaPlayerDevice): self._power.listeners.append(self) self.atv.push_updater.listener = self - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Handle when an entity is about to be added to Home Assistant.""" self._power.init() @@ -164,10 +161,9 @@ class AppleTvDevice(MediaPlayerDevice): if state in (STATE_PLAYING, STATE_PAUSED): return dt_util.utcnow() - @asyncio.coroutine - def async_play_media(self, media_type, media_id, **kwargs): + async def async_play_media(self, media_type, media_id, **kwargs): """Send the play_media command to the media player.""" - yield from self.atv.airplay.play_url(media_id) + await self.atv.airplay.play_url(media_id) @property def media_image_hash(self): @@ -176,12 +172,11 @@ class AppleTvDevice(MediaPlayerDevice): if self._playing and state not in [STATE_OFF, STATE_IDLE]: return self._playing.hash - @asyncio.coroutine - def async_get_media_image(self): + async def async_get_media_image(self): """Fetch media image of current playing image.""" state = self.state if self._playing and state not in [STATE_OFF, STATE_IDLE]: - return (yield from self.atv.metadata.artwork()), 'image/png' + return (await self.atv.metadata.artwork()), 'image/png' return None, None @@ -201,13 +196,11 @@ class AppleTvDevice(MediaPlayerDevice): """Flag media player features that are supported.""" return SUPPORT_APPLE_TV - @asyncio.coroutine - def async_turn_on(self): + async def async_turn_on(self): """Turn the media player on.""" self._power.set_power_on(True) - @asyncio.coroutine - def async_turn_off(self): + async def async_turn_off(self): """Turn the media player off.""" self._playing = None self._power.set_power_on(False) diff --git a/homeassistant/components/media_player/clementine.py b/homeassistant/components/media_player/clementine.py index e38c44b8d27..2add2bd682a 100644 --- a/homeassistant/components/media_player/clementine.py +++ b/homeassistant/components/media_player/clementine.py @@ -4,7 +4,6 @@ Support for Clementine Music Player as media player. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/media_player.clementine/ """ -import asyncio from datetime import timedelta import logging import time @@ -169,8 +168,7 @@ class ClementineDevice(MediaPlayerDevice): return None - @asyncio.coroutine - def async_get_media_image(self): + async def async_get_media_image(self): """Fetch media image of current playing image.""" if self._client.current_track: image = bytes(self._client.current_track['art']) diff --git a/homeassistant/components/media_player/emby.py b/homeassistant/components/media_player/emby.py index 2bf3a1b803f..50d2426c315 100644 --- a/homeassistant/components/media_player/emby.py +++ b/homeassistant/components/media_player/emby.py @@ -4,7 +4,6 @@ Support to interface with the Emby API. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/media_player.emby/ """ -import asyncio import logging import voluptuous as vol @@ -50,9 +49,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the Emby platform.""" from pyemby import EmbyServer @@ -113,10 +111,9 @@ def async_setup_platform(hass, config, async_add_entities, """Start Emby connection.""" emby.start() - @asyncio.coroutine - def stop_emby(event): + async def stop_emby(event): """Stop Emby connection.""" - yield from emby.stop() + await emby.stop() emby.add_new_devices_callback(device_update_callback) emby.add_stale_devices_callback(device_removal_callback) @@ -141,8 +138,7 @@ class EmbyDevice(MediaPlayerDevice): self.media_status_last_position = None self.media_status_received = None - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Register callback.""" self.emby.add_update_callback( self.async_update_callback, self.device_id) diff --git a/homeassistant/components/media_player/frontier_silicon.py b/homeassistant/components/media_player/frontier_silicon.py index aebdb676859..67c84bd7b1b 100644 --- a/homeassistant/components/media_player/frontier_silicon.py +++ b/homeassistant/components/media_player/frontier_silicon.py @@ -4,7 +4,6 @@ Support for Frontier Silicon Devices (Medion, Hama, Auna,...). For more details about this platform, please refer to the documentation at https://home-assistant.io/components/media_player.frontier_silicon/ """ -import asyncio import logging import voluptuous as vol @@ -41,8 +40,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform( +async def async_setup_platform( hass, config, async_add_entities, discovery_info=None): """Set up the Frontier Silicon platform.""" import requests @@ -157,18 +155,17 @@ class AFSAPIDevice(MediaPlayerDevice): """Image url of current playing media.""" return self._media_image_url - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Get the latest date and update device state.""" fs_device = self.fs_device if not self._name: - self._name = yield from fs_device.get_friendly_name() + self._name = await fs_device.get_friendly_name() if not self._source_list: - self._source_list = yield from fs_device.get_mode_list() + self._source_list = await fs_device.get_mode_list() - status = yield from fs_device.get_play_status() + status = await fs_device.get_play_status() self._state = { 'playing': STATE_PLAYING, 'paused': STATE_PAUSED, @@ -178,16 +175,16 @@ class AFSAPIDevice(MediaPlayerDevice): }.get(status, STATE_UNKNOWN) if self._state != STATE_OFF: - info_name = yield from fs_device.get_play_name() - info_text = yield from fs_device.get_play_text() + info_name = await fs_device.get_play_name() + info_text = await fs_device.get_play_text() self._title = ' - '.join(filter(None, [info_name, info_text])) - self._artist = yield from fs_device.get_play_artist() - self._album_name = yield from fs_device.get_play_album() + self._artist = await fs_device.get_play_artist() + self._album_name = await fs_device.get_play_album() - self._source = yield from fs_device.get_mode() - self._mute = yield from fs_device.get_mute() - self._media_image_url = yield from fs_device.get_play_graphic() + self._source = await fs_device.get_mode() + self._mute = await fs_device.get_mute() + self._media_image_url = await fs_device.get_play_graphic() else: self._title = None self._artist = None @@ -199,48 +196,40 @@ class AFSAPIDevice(MediaPlayerDevice): # Management actions # power control - @asyncio.coroutine - def async_turn_on(self): + async def async_turn_on(self): """Turn on the device.""" - yield from self.fs_device.set_power(True) + await self.fs_device.set_power(True) - @asyncio.coroutine - def async_turn_off(self): + async def async_turn_off(self): """Turn off the device.""" - yield from self.fs_device.set_power(False) + await self.fs_device.set_power(False) - @asyncio.coroutine - def async_media_play(self): + async def async_media_play(self): """Send play command.""" - yield from self.fs_device.play() + await self.fs_device.play() - @asyncio.coroutine - def async_media_pause(self): + async def async_media_pause(self): """Send pause command.""" - yield from self.fs_device.pause() + await self.fs_device.pause() - @asyncio.coroutine - def async_media_play_pause(self): + async def async_media_play_pause(self): """Send play/pause command.""" if 'playing' in self._state: - yield from self.fs_device.pause() + await self.fs_device.pause() else: - yield from self.fs_device.play() + await self.fs_device.play() - @asyncio.coroutine - def async_media_stop(self): + async def async_media_stop(self): """Send play/pause command.""" - yield from self.fs_device.pause() + await self.fs_device.pause() - @asyncio.coroutine - def async_media_previous_track(self): + async def async_media_previous_track(self): """Send previous track command (results in rewind).""" - yield from self.fs_device.rewind() + await self.fs_device.rewind() - @asyncio.coroutine - def async_media_next_track(self): + async def async_media_next_track(self): """Send next track command (results in fast-forward).""" - yield from self.fs_device.forward() + await self.fs_device.forward() # mute @property @@ -248,30 +237,25 @@ class AFSAPIDevice(MediaPlayerDevice): """Boolean if volume is currently muted.""" return self._mute - @asyncio.coroutine - def async_mute_volume(self, mute): + async def async_mute_volume(self, mute): """Send mute command.""" - yield from self.fs_device.set_mute(mute) + await self.fs_device.set_mute(mute) # volume - @asyncio.coroutine - def async_volume_up(self): + async def async_volume_up(self): """Send volume up command.""" - volume = yield from self.fs_device.get_volume() - yield from self.fs_device.set_volume(volume+1) + volume = await self.fs_device.get_volume() + await self.fs_device.set_volume(volume+1) - @asyncio.coroutine - def async_volume_down(self): + async def async_volume_down(self): """Send volume down command.""" - volume = yield from self.fs_device.get_volume() - yield from self.fs_device.set_volume(volume-1) + volume = await self.fs_device.get_volume() + await self.fs_device.set_volume(volume-1) - @asyncio.coroutine - def async_set_volume_level(self, volume): + async def async_set_volume_level(self, volume): """Set volume command.""" - yield from self.fs_device.set_volume(volume) + await self.fs_device.set_volume(volume) - @asyncio.coroutine - def async_select_source(self, source): + async def async_select_source(self, source): """Select input source.""" - yield from self.fs_device.set_mode(source) + await self.fs_device.set_mode(source) diff --git a/homeassistant/components/media_player/kodi.py b/homeassistant/components/media_player/kodi.py index c98dc5c56fe..eb97c75c9ef 100644 --- a/homeassistant/components/media_player/kodi.py +++ b/homeassistant/components/media_player/kodi.py @@ -156,9 +156,8 @@ def _check_deprecated_turn_off(hass, turn_off_action): return turn_off_action -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the Kodi platform.""" if DATA_KODI not in hass.data: hass.data[DATA_KODI] = dict() @@ -211,8 +210,7 @@ def async_setup_platform(hass, config, async_add_entities, hass.data[DATA_KODI][ip_addr] = entity async_add_entities([entity], update_before_add=True) - @asyncio.coroutine - def async_service_handler(service): + async def async_service_handler(service): """Map services to methods on MediaPlayerDevice.""" method = SERVICE_TO_METHOD.get(service.service) if not method: @@ -230,7 +228,7 @@ def async_setup_platform(hass, config, async_add_entities, update_tasks = [] for player in target_players: - yield from getattr(player, method['method'])(**params) + await getattr(player, method['method'])(**params) for player in target_players: if player.should_poll: @@ -238,7 +236,7 @@ def async_setup_platform(hass, config, async_add_entities, update_tasks.append(update_coro) if update_tasks: - yield from asyncio.wait(update_tasks, loop=hass.loop) + await asyncio.wait(update_tasks, loop=hass.loop) if hass.services.has_service(DOMAIN, SERVICE_ADD_MEDIA): return @@ -253,12 +251,11 @@ def async_setup_platform(hass, config, async_add_entities, def cmd(func): """Catch command exceptions.""" @wraps(func) - @asyncio.coroutine - def wrapper(obj, *args, **kwargs): + async def wrapper(obj, *args, **kwargs): """Wrap all command methods.""" import jsonrpc_base try: - yield from func(obj, *args, **kwargs) + await func(obj, *args, **kwargs) except jsonrpc_base.jsonrpc.TransportError as exc: # If Kodi is off, we expect calls to fail. if obj.state == STATE_OFF: @@ -392,12 +389,11 @@ class KodiDevice(MediaPlayerDevice): self._app_properties = {} self.hass.async_add_job(self._ws_server.close()) - @asyncio.coroutine - def _get_players(self): + async def _get_players(self): """Return the active player objects or None.""" import jsonrpc_base try: - return (yield from self.server.Player.GetActivePlayers()) + return await self.server.Player.GetActivePlayers() except jsonrpc_base.jsonrpc.TransportError: if self._players is not None: _LOGGER.info("Unable to fetch kodi data") @@ -423,23 +419,21 @@ class KodiDevice(MediaPlayerDevice): return STATE_PLAYING - @asyncio.coroutine - def async_ws_connect(self): + async def async_ws_connect(self): """Connect to Kodi via websocket protocol.""" import jsonrpc_base try: - ws_loop_future = yield from self._ws_server.ws_connect() + ws_loop_future = await self._ws_server.ws_connect() except jsonrpc_base.jsonrpc.TransportError: _LOGGER.info("Unable to connect to Kodi via websocket") _LOGGER.debug( "Unable to connect to Kodi via websocket", exc_info=True) return - @asyncio.coroutine - def ws_loop_wrapper(): + async def ws_loop_wrapper(): """Catch exceptions from the websocket loop task.""" try: - yield from ws_loop_future + await ws_loop_future except jsonrpc_base.TransportError: # Kodi abruptly ends ws connection when exiting. We will try # to reconnect on the next poll. @@ -451,10 +445,9 @@ class KodiDevice(MediaPlayerDevice): # run until the websocket connection is closed. self.hass.loop.create_task(ws_loop_wrapper()) - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Retrieve latest state.""" - self._players = yield from self._get_players() + self._players = await self._get_players() if self._players is None: self._properties = {} @@ -466,7 +459,7 @@ class KodiDevice(MediaPlayerDevice): self.hass.async_add_job(self.async_ws_connect()) self._app_properties = \ - yield from self.server.Application.GetProperties( + await self.server.Application.GetProperties( ['volume', 'muted'] ) @@ -475,12 +468,12 @@ class KodiDevice(MediaPlayerDevice): assert isinstance(player_id, int) - self._properties = yield from self.server.Player.GetProperties( + self._properties = await self.server.Player.GetProperties( player_id, ['time', 'totaltime', 'speed', 'live'] ) - self._item = (yield from self.server.Player.GetItem( + self._item = (await self.server.Player.GetItem( player_id, ['title', 'file', 'uniqueid', 'thumbnail', 'artist', 'albumartist', 'showtitle', 'album', 'season', 'episode'] @@ -622,38 +615,34 @@ class KodiDevice(MediaPlayerDevice): return supported_features @cmd - @asyncio.coroutine - def async_turn_on(self): + async def async_turn_on(self): """Execute turn_on_action to turn on media player.""" if self._turn_on_action is not None: - yield from self._turn_on_action.async_run( + await self._turn_on_action.async_run( variables={"entity_id": self.entity_id}) else: _LOGGER.warning("turn_on requested but turn_on_action is none") @cmd - @asyncio.coroutine - def async_turn_off(self): + async def async_turn_off(self): """Execute turn_off_action to turn off media player.""" if self._turn_off_action is not None: - yield from self._turn_off_action.async_run( + await self._turn_off_action.async_run( variables={"entity_id": self.entity_id}) else: _LOGGER.warning("turn_off requested but turn_off_action is none") @cmd - @asyncio.coroutine - def async_volume_up(self): + async def async_volume_up(self): """Volume up the media player.""" assert ( - yield from self.server.Input.ExecuteAction('volumeup')) == 'OK' + await self.server.Input.ExecuteAction('volumeup')) == 'OK' @cmd - @asyncio.coroutine - def async_volume_down(self): + async def async_volume_down(self): """Volume down the media player.""" assert ( - yield from self.server.Input.ExecuteAction('volumedown')) == 'OK' + await self.server.Input.ExecuteAction('volumedown')) == 'OK' @cmd def async_set_volume_level(self, volume): @@ -671,13 +660,12 @@ class KodiDevice(MediaPlayerDevice): """ return self.server.Application.SetMute(mute) - @asyncio.coroutine - def async_set_play_state(self, state): + async def async_set_play_state(self, state): """Handle play/pause/toggle.""" - players = yield from self._get_players() + players = await self._get_players() if players is not None and players: - yield from self.server.Player.PlayPause( + await self.server.Player.PlayPause( players[0]['playerid'], state) @cmd @@ -705,26 +693,24 @@ class KodiDevice(MediaPlayerDevice): return self.async_set_play_state(False) @cmd - @asyncio.coroutine - def async_media_stop(self): + async def async_media_stop(self): """Stop the media player.""" - players = yield from self._get_players() + players = await self._get_players() if players: - yield from self.server.Player.Stop(players[0]['playerid']) + await self.server.Player.Stop(players[0]['playerid']) - @asyncio.coroutine - def _goto(self, direction): + async def _goto(self, direction): """Handle for previous/next track.""" - players = yield from self._get_players() + players = await self._get_players() if players: if direction == 'previous': # First seek to position 0. Kodi goes to the beginning of the # current track if the current track is not at the beginning. - yield from self.server.Player.Seek(players[0]['playerid'], 0) + await self.server.Player.Seek(players[0]['playerid'], 0) - yield from self.server.Player.GoTo( + await self.server.Player.GoTo( players[0]['playerid'], direction) @cmd @@ -744,10 +730,9 @@ class KodiDevice(MediaPlayerDevice): return self._goto('previous') @cmd - @asyncio.coroutine - def async_media_seek(self, position): + async def async_media_seek(self, position): """Send seek command.""" - players = yield from self._get_players() + players = await self._get_players() time = {} @@ -763,7 +748,7 @@ class KodiDevice(MediaPlayerDevice): time['hours'] = int(position) if players: - yield from self.server.Player.Seek(players[0]['playerid'], time) + await self.server.Player.Seek(players[0]['playerid'], time) @cmd def async_play_media(self, media_type, media_id, **kwargs): @@ -781,22 +766,20 @@ class KodiDevice(MediaPlayerDevice): return self.server.Player.Open( {"item": {"file": str(media_id)}}) - @asyncio.coroutine - def async_set_shuffle(self, shuffle): + async def async_set_shuffle(self, shuffle): """Set shuffle mode, for the first player.""" if not self._players: raise RuntimeError("Error: No active player.") - yield from self.server.Player.SetShuffle( + await self.server.Player.SetShuffle( {"playerid": self._players[0]['playerid'], "shuffle": shuffle}) - @asyncio.coroutine - def async_call_method(self, method, **kwargs): + async def async_call_method(self, method, **kwargs): """Run Kodi JSONRPC API method with params.""" import jsonrpc_base _LOGGER.debug("Run API method %s, kwargs=%s", method, kwargs) result_ok = False try: - result = yield from getattr(self.server, method)(**kwargs) + result = await getattr(self.server, method)(**kwargs) result_ok = True except jsonrpc_base.jsonrpc.ProtocolError as exc: result = exc.args[2]['error'] @@ -817,8 +800,7 @@ class KodiDevice(MediaPlayerDevice): event_data=event_data) return result - @asyncio.coroutine - def async_add_media_to_playlist( + async def async_add_media_to_playlist( self, media_type, media_id=None, media_name='ALL', artist_name=''): """Add a media to default playlist (i.e. playlistid=0). @@ -832,7 +814,7 @@ class KodiDevice(MediaPlayerDevice): params = {"playlistid": 0} if media_type == "SONG": if media_id is None: - media_id = yield from self.async_find_song( + media_id = await self.async_find_song( media_name, artist_name) if media_id: params["item"] = {"songid": int(media_id)} @@ -840,10 +822,10 @@ class KodiDevice(MediaPlayerDevice): elif media_type == "ALBUM": if media_id is None: if media_name == "ALL": - yield from self.async_add_all_albums(artist_name) + await self.async_add_all_albums(artist_name) return - media_id = yield from self.async_find_album( + media_id = await self.async_find_album( media_name, artist_name) if media_id: params["item"] = {"albumid": int(media_id)} @@ -853,7 +835,7 @@ class KodiDevice(MediaPlayerDevice): if media_id is not None: try: - yield from self.server.Playlist.Add(params) + await self.server.Playlist.Add(params) except jsonrpc_base.jsonrpc.ProtocolError as exc: result = exc.args[2]['error'] _LOGGER.error("Run API method %s.Playlist.Add(%s) error: %s", @@ -864,43 +846,38 @@ class KodiDevice(MediaPlayerDevice): else: _LOGGER.warning("No media detected for Playlist.Add") - @asyncio.coroutine - def async_add_all_albums(self, artist_name): + async def async_add_all_albums(self, artist_name): """Add all albums of an artist to default playlist (i.e. playlistid=0). The artist is specified in terms of name. """ - artist_id = yield from self.async_find_artist(artist_name) + artist_id = await self.async_find_artist(artist_name) - albums = yield from self.async_get_albums(artist_id) + albums = await self.async_get_albums(artist_id) for alb in albums['albums']: - yield from self.server.Playlist.Add( + await self.server.Playlist.Add( {"playlistid": 0, "item": {"albumid": int(alb['albumid'])}}) - @asyncio.coroutine - def async_clear_playlist(self): + async def async_clear_playlist(self): """Clear default playlist (i.e. playlistid=0).""" return self.server.Playlist.Clear({"playlistid": 0}) - @asyncio.coroutine - def async_get_artists(self): + async def async_get_artists(self): """Get artists list.""" - return (yield from self.server.AudioLibrary.GetArtists()) + return await self.server.AudioLibrary.GetArtists() - @asyncio.coroutine - def async_get_albums(self, artist_id=None): + async def async_get_albums(self, artist_id=None): """Get albums list.""" if artist_id is None: - return (yield from self.server.AudioLibrary.GetAlbums()) + return await self.server.AudioLibrary.GetAlbums() - return (yield from self.server.AudioLibrary.GetAlbums( + return (await self.server.AudioLibrary.GetAlbums( {"filter": {"artistid": int(artist_id)}})) - @asyncio.coroutine - def async_find_artist(self, artist_name): + async def async_find_artist(self, artist_name): """Find artist by name.""" - artists = yield from self.async_get_artists() + artists = await self.async_get_artists() try: out = self._find( artist_name, [a['artist'] for a in artists['artists']]) @@ -909,37 +886,34 @@ class KodiDevice(MediaPlayerDevice): _LOGGER.warning("No artists were found: %s", artist_name) return None - @asyncio.coroutine - def async_get_songs(self, artist_id=None): + async def async_get_songs(self, artist_id=None): """Get songs list.""" if artist_id is None: - return (yield from self.server.AudioLibrary.GetSongs()) + return await self.server.AudioLibrary.GetSongs() - return (yield from self.server.AudioLibrary.GetSongs( + return (await self.server.AudioLibrary.GetSongs( {"filter": {"artistid": int(artist_id)}})) - @asyncio.coroutine - def async_find_song(self, song_name, artist_name=''): + async def async_find_song(self, song_name, artist_name=''): """Find song by name and optionally artist name.""" artist_id = None if artist_name != '': - artist_id = yield from self.async_find_artist(artist_name) + artist_id = await self.async_find_artist(artist_name) - songs = yield from self.async_get_songs(artist_id) + songs = await self.async_get_songs(artist_id) if songs['limits']['total'] == 0: return None out = self._find(song_name, [a['label'] for a in songs['songs']]) return songs['songs'][out[0][0]]['songid'] - @asyncio.coroutine - def async_find_album(self, album_name, artist_name=''): + async def async_find_album(self, album_name, artist_name=''): """Find album by name and optionally artist name.""" artist_id = None if artist_name != '': - artist_id = yield from self.async_find_artist(artist_name) + artist_id = await self.async_find_artist(artist_name) - albums = yield from self.async_get_albums(artist_id) + albums = await self.async_get_albums(artist_id) try: out = self._find( album_name, [a['label'] for a in albums['albums']]) diff --git a/homeassistant/components/media_player/liveboxplaytv.py b/homeassistant/components/media_player/liveboxplaytv.py index 9a08ceeac93..3f8ea2cfd48 100644 --- a/homeassistant/components/media_player/liveboxplaytv.py +++ b/homeassistant/components/media_player/liveboxplaytv.py @@ -4,7 +4,6 @@ Support for interface with an Orange Livebox Play TV appliance. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/media_player.liveboxplaytv/ """ -import asyncio from datetime import timedelta import logging @@ -44,9 +43,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the Orange Livebox Play TV platform.""" host = config.get(CONF_HOST) port = config.get(CONF_PORT) @@ -83,8 +81,7 @@ class LiveboxPlayTvDevice(MediaPlayerDevice): self._media_image_url = None self._media_last_updated = None - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Retrieve the latest data.""" import pyteleloisirs try: @@ -95,7 +92,7 @@ class LiveboxPlayTvDevice(MediaPlayerDevice): channel = self._client.channel if channel is not None: self._current_channel = channel - program = yield from \ + program = await \ self._client.async_get_current_program() if program and self._current_program != program.get('name'): self._current_program = program.get('name') @@ -109,7 +106,7 @@ class LiveboxPlayTvDevice(MediaPlayerDevice): # Set media image to current program if a thumbnail is # available. Otherwise we'll use the channel's image. img_size = 800 - prg_img_url = yield from \ + prg_img_url = await \ self._client.async_get_current_program_image(img_size) if prg_img_url: self._media_image_url = prg_img_url diff --git a/homeassistant/components/media_player/russound_rio.py b/homeassistant/components/media_player/russound_rio.py index 74f6bfb35ab..19cc2228d32 100644 --- a/homeassistant/components/media_player/russound_rio.py +++ b/homeassistant/components/media_player/russound_rio.py @@ -4,7 +4,6 @@ Support for Russound multizone controllers using RIO Protocol. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/media_player.russound_rio/ """ -import asyncio import logging import voluptuous as vol @@ -33,8 +32,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform( +async def async_setup_platform( hass, config, async_add_entities, discovery_info=None): """Set up the Russound RIO platform.""" from russound_rio import Russound @@ -44,15 +42,15 @@ def async_setup_platform( russ = Russound(hass.loop, host, port) - yield from russ.connect() + await russ.connect() # Discover sources and zones - sources = yield from russ.enumerate_sources() - valid_zones = yield from russ.enumerate_zones() + sources = await russ.enumerate_sources() + valid_zones = await russ.enumerate_zones() devices = [] for zone_id, name in valid_zones: - yield from russ.watch_zone(zone_id) + await russ.watch_zone(zone_id) dev = RussoundZoneDevice(russ, zone_id, name, sources) devices.append(dev) @@ -108,8 +106,7 @@ class RussoundZoneDevice(MediaPlayerDevice): if source_id == current: self.schedule_update_ha_state() - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Register callback handlers.""" self._russ.add_zone_callback(self._zone_callback_handler) self._russ.add_source_callback(self._source_callback_handler) diff --git a/homeassistant/components/media_player/snapcast.py b/homeassistant/components/media_player/snapcast.py index 84864caeed1..fca440df783 100644 --- a/homeassistant/components/media_player/snapcast.py +++ b/homeassistant/components/media_player/snapcast.py @@ -4,7 +4,6 @@ Support for interacting with Snapcast clients. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/media_player.snapcast/ """ -import asyncio import logging import socket @@ -46,17 +45,15 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the Snapcast platform.""" import snapcast.control from snapcast.control.server import CONTROL_PORT host = config.get(CONF_HOST) port = config.get(CONF_PORT, CONTROL_PORT) - @asyncio.coroutine - def _handle_service(service): + async def _handle_service(service): """Handle services.""" entity_ids = service.data.get(ATTR_ENTITY_ID) devices = [device for device in hass.data[DATA_KEY] @@ -65,7 +62,7 @@ def async_setup_platform(hass, config, async_add_entities, if service.service == SERVICE_SNAPSHOT: device.snapshot() elif service.service == SERVICE_RESTORE: - yield from device.async_restore() + await device.async_restore() hass.services.async_register( DOMAIN, SERVICE_SNAPSHOT, _handle_service, schema=SERVICE_SCHEMA) @@ -73,7 +70,7 @@ def async_setup_platform(hass, config, async_add_entities, DOMAIN, SERVICE_RESTORE, _handle_service, schema=SERVICE_SCHEMA) try: - server = yield from snapcast.control.create_server( + server = await snapcast.control.create_server( hass.loop, host, port, reconnect=True) except socket.gaierror: _LOGGER.error("Could not connect to Snapcast server at %s:%d", @@ -157,34 +154,30 @@ class SnapcastGroupDevice(MediaPlayerDevice): """Do not poll for state.""" return False - @asyncio.coroutine - def async_select_source(self, source): + async def async_select_source(self, source): """Set input source.""" streams = self._group.streams_by_name() if source in streams: - yield from self._group.set_stream(streams[source].identifier) + await self._group.set_stream(streams[source].identifier) self.async_schedule_update_ha_state() - @asyncio.coroutine - def async_mute_volume(self, mute): + async def async_mute_volume(self, mute): """Send the mute command.""" - yield from self._group.set_muted(mute) + await self._group.set_muted(mute) self.async_schedule_update_ha_state() - @asyncio.coroutine - def async_set_volume_level(self, volume): + async def async_set_volume_level(self, volume): """Set the volume level.""" - yield from self._group.set_volume(round(volume * 100)) + await self._group.set_volume(round(volume * 100)) self.async_schedule_update_ha_state() def snapshot(self): """Snapshot the group state.""" self._group.snapshot() - @asyncio.coroutine - def async_restore(self): + async def async_restore(self): """Restore the group state.""" - yield from self._group.restore() + await self._group.restore() class SnapcastClientDevice(MediaPlayerDevice): @@ -246,23 +239,20 @@ class SnapcastClientDevice(MediaPlayerDevice): """Do not poll for state.""" return False - @asyncio.coroutine - def async_mute_volume(self, mute): + async def async_mute_volume(self, mute): """Send the mute command.""" - yield from self._client.set_muted(mute) + await self._client.set_muted(mute) self.async_schedule_update_ha_state() - @asyncio.coroutine - def async_set_volume_level(self, volume): + async def async_set_volume_level(self, volume): """Set the volume level.""" - yield from self._client.set_volume(round(volume * 100)) + await self._client.set_volume(round(volume * 100)) self.async_schedule_update_ha_state() def snapshot(self): """Snapshot the client state.""" self._client.snapshot() - @asyncio.coroutine - def async_restore(self): + async def async_restore(self): """Restore the client state.""" - yield from self._client.restore() + await self._client.restore() diff --git a/homeassistant/components/media_player/sonos.py b/homeassistant/components/media_player/sonos.py index 1486d47e759..07567590bdc 100644 --- a/homeassistant/components/media_player/sonos.py +++ b/homeassistant/components/media_player/sonos.py @@ -4,7 +4,6 @@ Support to interface with Sonos players. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/media_player.sonos/ """ -import asyncio import datetime import functools as ft import logging @@ -364,8 +363,7 @@ class SonosDevice(MediaPlayerDevice): self._set_basic_information() - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Subscribe sonos events.""" self.hass.data[DATA_SONOS].devices.append(self) self.hass.async_add_job(self._subscribe_to_player_events) diff --git a/homeassistant/components/media_player/squeezebox.py b/homeassistant/components/media_player/squeezebox.py index 2d6a849aecb..f8347830141 100644 --- a/homeassistant/components/media_player/squeezebox.py +++ b/homeassistant/components/media_player/squeezebox.py @@ -64,9 +64,8 @@ SERVICE_TO_METHOD = { } -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the squeezebox platform.""" import socket @@ -106,13 +105,12 @@ def async_setup_platform(hass, config, async_add_entities, _LOGGER.debug("Creating LMS object for %s", ipaddr) lms = LogitechMediaServer(hass, host, port, username, password) - players = yield from lms.create_players() + players = await lms.create_players() hass.data[DATA_SQUEEZEBOX].extend(players) async_add_entities(players) - @asyncio.coroutine - def async_service_handler(service): + async def async_service_handler(service): """Map services to methods on MediaPlayerDevice.""" method = SERVICE_TO_METHOD.get(service.service) if not method: @@ -129,11 +127,11 @@ def async_setup_platform(hass, config, async_add_entities, update_tasks = [] for player in target_players: - yield from getattr(player, method['method'])(**params) + await getattr(player, method['method'])(**params) update_tasks.append(player.async_update_ha_state(True)) if update_tasks: - yield from asyncio.wait(update_tasks, loop=hass.loop) + await asyncio.wait(update_tasks, loop=hass.loop) for service in SERVICE_TO_METHOD: schema = SERVICE_TO_METHOD[service]['schema'] @@ -155,22 +153,20 @@ class LogitechMediaServer: self._username = username self._password = password - @asyncio.coroutine - def create_players(self): + async def create_players(self): """Create a list of devices connected to LMS.""" result = [] - data = yield from self.async_query('players', 'status') + data = await self.async_query('players', 'status') if data is False: return result for players in data.get('players_loop', []): player = SqueezeBoxDevice( self, players['playerid'], players['name']) - yield from player.async_update() + await player.async_update() result.append(player) return result - @asyncio.coroutine - def async_query(self, *command, player=""): + async def async_query(self, *command, player=""): """Abstract out the JSON-RPC connection.""" auth = None if self._username is None else aiohttp.BasicAuth( self._username, self._password) @@ -187,7 +183,7 @@ class LogitechMediaServer: try: websession = async_get_clientsession(self.hass) with async_timeout.timeout(TIMEOUT, loop=self.hass.loop): - response = yield from websession.post( + response = await websession.post( url, data=data, auth=auth) @@ -198,7 +194,7 @@ class LogitechMediaServer: response.status, response) return False - data = yield from response.json() + data = await response.json() except (asyncio.TimeoutError, aiohttp.ClientError) as error: _LOGGER.error("Failed communicating with LMS: %s", type(error)) @@ -256,11 +252,10 @@ class SqueezeBoxDevice(MediaPlayerDevice): return self._lms.async_query( *parameters, player=self._id) - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Retrieve the current state of the player.""" tags = 'adKl' - response = yield from self.async_query( + response = await self.async_query( "status", "-", "1", "tags:{tags}" .format(tags=tags)) diff --git a/homeassistant/components/media_player/volumio.py b/homeassistant/components/media_player/volumio.py index 743f19cb259..de0f726c2ce 100644 --- a/homeassistant/components/media_player/volumio.py +++ b/homeassistant/components/media_player/volumio.py @@ -51,9 +51,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the Volumio platform.""" if DATA_VOLUMIO not in hass.data: hass.data[DATA_VOLUMIO] = dict() @@ -96,8 +95,7 @@ class Volumio(MediaPlayerDevice): self._playlists = [] self._currentplaylist = None - @asyncio.coroutine - def send_volumio_msg(self, method, params=None): + async def send_volumio_msg(self, method, params=None): """Send message.""" url = "http://{}:{}/api/v1/{}/".format(self.host, self.port, method) @@ -105,9 +103,9 @@ class Volumio(MediaPlayerDevice): try: websession = async_get_clientsession(self.hass) - response = yield from websession.get(url, params=params) + response = await websession.get(url, params=params) if response.status == 200: - data = yield from response.json() + data = await response.json() else: _LOGGER.error( "Query failed, response code: %s Full message: %s", @@ -124,11 +122,10 @@ class Volumio(MediaPlayerDevice): _LOGGER.error("Received invalid response: %s", data) return False - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Update state.""" - resp = yield from self.send_volumio_msg('getState') - yield from self._async_update_playlists() + resp = await self.send_volumio_msg('getState') + await self._async_update_playlists() if resp is False: return self._state = resp.copy() diff --git a/homeassistant/components/notify/__init__.py b/homeassistant/components/notify/__init__.py index 0535d7caa6d..f0320617e19 100644 --- a/homeassistant/components/notify/__init__.py +++ b/homeassistant/components/notify/__init__.py @@ -49,18 +49,16 @@ NOTIFY_SERVICE_SCHEMA = vol.Schema({ }) -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Set up the notify services.""" targets = {} - @asyncio.coroutine - def async_setup_platform(p_type, p_config=None, discovery_info=None): + async def async_setup_platform(p_type, p_config=None, discovery_info=None): """Set up a notify platform.""" if p_config is None: p_config = {} - platform = yield from async_prepare_setup_platform( + platform = await async_prepare_setup_platform( hass, config, DOMAIN, p_type) if platform is None: @@ -71,10 +69,10 @@ def async_setup(hass, config): notify_service = None try: if hasattr(platform, 'async_get_service'): - notify_service = yield from \ + notify_service = await \ platform.async_get_service(hass, p_config, discovery_info) elif hasattr(platform, 'get_service'): - notify_service = yield from hass.async_add_job( + notify_service = await hass.async_add_job( platform.get_service, hass, p_config, discovery_info) else: raise HomeAssistantError("Invalid notify platform.") @@ -97,8 +95,7 @@ def async_setup(hass, config): if discovery_info is None: discovery_info = {} - @asyncio.coroutine - def async_notify_message(service): + async def async_notify_message(service): """Handle sending notification message service calls.""" kwargs = {} message = service.data[ATTR_MESSAGE] @@ -117,7 +114,7 @@ def async_setup(hass, config): kwargs[ATTR_MESSAGE] = message.async_render() kwargs[ATTR_DATA] = service.data.get(ATTR_DATA) - yield from notify_service.async_send_message(**kwargs) + await notify_service.async_send_message(**kwargs) if hasattr(notify_service, 'targets'): platform_name = ( @@ -147,12 +144,11 @@ def async_setup(hass, config): in config_per_platform(config, DOMAIN)] if setup_tasks: - yield from asyncio.wait(setup_tasks, loop=hass.loop) + await asyncio.wait(setup_tasks, loop=hass.loop) - @asyncio.coroutine - def async_platform_discovered(platform, info): + async def async_platform_discovered(platform, info): """Handle for discovered platform.""" - yield from async_setup_platform(platform, discovery_info=info) + await async_setup_platform(platform, discovery_info=info) discovery.async_listen_platform(hass, DOMAIN, async_platform_discovered) diff --git a/homeassistant/components/notify/discord.py b/homeassistant/components/notify/discord.py index 0cf4bced360..8bd4e27155d 100644 --- a/homeassistant/components/notify/discord.py +++ b/homeassistant/components/notify/discord.py @@ -4,7 +4,6 @@ Discord platform for notify component. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/notify.discord/ """ -import asyncio import logging import voluptuous as vol @@ -39,8 +38,7 @@ class DiscordNotificationService(BaseNotificationService): self.token = token self.hass = hass - @asyncio.coroutine - def async_send_message(self, message, **kwargs): + async def async_send_message(self, message, **kwargs): """Login to Discord, send message to channel(s) and log out.""" import discord discord_bot = discord.Client(loop=self.hass.loop) @@ -51,8 +49,7 @@ class DiscordNotificationService(BaseNotificationService): # pylint: disable=unused-variable @discord_bot.event - @asyncio.coroutine - def on_ready(): + async def on_ready(): """Send the messages when the bot is ready.""" try: data = kwargs.get(ATTR_DATA) @@ -60,14 +57,14 @@ class DiscordNotificationService(BaseNotificationService): images = data.get(ATTR_IMAGES) for channelid in kwargs[ATTR_TARGET]: channel = discord.Object(id=channelid) - yield from discord_bot.send_message(channel, message) + await discord_bot.send_message(channel, message) if images: for anum, f_name in enumerate(images): - yield from discord_bot.send_file(channel, f_name) + await discord_bot.send_file(channel, f_name) except (discord.errors.HTTPException, discord.errors.NotFound) as error: _LOGGER.warning("Communication error: %s", error) - yield from discord_bot.logout() - yield from discord_bot.close() + await discord_bot.logout() + await discord_bot.close() - yield from discord_bot.start(self.token) + await discord_bot.start(self.token) diff --git a/homeassistant/components/notify/group.py b/homeassistant/components/notify/group.py index 94856c730b1..5d25c2d815e 100644 --- a/homeassistant/components/notify/group.py +++ b/homeassistant/components/notify/group.py @@ -41,8 +41,7 @@ def update(input_dict, update_source): return input_dict -@asyncio.coroutine -def async_get_service(hass, config, discovery_info=None): +async def async_get_service(hass, config, discovery_info=None): """Get the Group notification service.""" return GroupNotifyPlatform(hass, config.get(CONF_SERVICES)) @@ -55,8 +54,7 @@ class GroupNotifyPlatform(BaseNotificationService): self.hass = hass self.entities = entities - @asyncio.coroutine - def async_send_message(self, message="", **kwargs): + async def async_send_message(self, message="", **kwargs): """Send message to all entities in the group.""" payload = {ATTR_MESSAGE: message} payload.update({key: val for key, val in kwargs.items() if val}) @@ -70,4 +68,4 @@ class GroupNotifyPlatform(BaseNotificationService): DOMAIN, entity.get(ATTR_SERVICE), sending_payload)) if tasks: - yield from asyncio.wait(tasks, loop=self.hass.loop) + await asyncio.wait(tasks, loop=self.hass.loop) diff --git a/homeassistant/components/notify/kodi.py b/homeassistant/components/notify/kodi.py index 3eb492f7fa6..74bfe61d3f2 100644 --- a/homeassistant/components/notify/kodi.py +++ b/homeassistant/components/notify/kodi.py @@ -4,7 +4,6 @@ Kodi notification service. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/notify.kodi/ """ -import asyncio import logging import aiohttp @@ -38,8 +37,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ ATTR_DISPLAYTIME = 'displaytime' -@asyncio.coroutine -def async_get_service(hass, config, discovery_info=None): +async def async_get_service(hass, config, discovery_info=None): """Return the notify service.""" url = '{}:{}'.format(config.get(CONF_HOST), config.get(CONF_PORT)) @@ -86,8 +84,7 @@ class KodiNotificationService(BaseNotificationService): self._server = jsonrpc_async.Server(self._url, **kwargs) - @asyncio.coroutine - def async_send_message(self, message="", **kwargs): + async def async_send_message(self, message="", **kwargs): """Send a message to Kodi.""" import jsonrpc_async try: @@ -96,7 +93,7 @@ class KodiNotificationService(BaseNotificationService): displaytime = data.get(ATTR_DISPLAYTIME, 10000) icon = data.get(ATTR_ICON, "info") title = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT) - yield from self._server.GUI.ShowNotification( + await self._server.GUI.ShowNotification( title, message, icon, displaytime) except jsonrpc_async.TransportError: diff --git a/homeassistant/components/notify/prowl.py b/homeassistant/components/notify/prowl.py index 3928fa81167..f0741766a70 100644 --- a/homeassistant/components/notify/prowl.py +++ b/homeassistant/components/notify/prowl.py @@ -25,8 +25,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_get_service(hass, config, discovery_info=None): +async def async_get_service(hass, config, discovery_info=None): """Get the Prowl notification service.""" return ProwlNotificationService(hass, config[CONF_API_KEY]) @@ -39,8 +38,7 @@ class ProwlNotificationService(BaseNotificationService): self._hass = hass self._api_key = api_key - @asyncio.coroutine - def async_send_message(self, message, **kwargs): + async def async_send_message(self, message, **kwargs): """Send the message to the user.""" response = None session = None @@ -59,8 +57,8 @@ class ProwlNotificationService(BaseNotificationService): try: with async_timeout.timeout(10, loop=self._hass.loop): - response = yield from session.post(url, data=payload) - result = yield from response.text() + response = await session.post(url, data=payload) + result = await response.text() if response.status != 200 or 'error' in result: _LOGGER.error("Prowl service returned http " From b24f9f5dfaeb44f8b8c7c9848368497d3437404b Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Mon, 1 Oct 2018 08:59:45 +0200 Subject: [PATCH 124/247] Async syntax 3/8 (#17017) * Async syntax 3, device_tracker & fan & hassio & image_processing & input * Pylint fixes --- .../components/device_tracker/__init__.py | 84 ++++++++----------- .../components/device_tracker/geofency.py | 15 ++-- .../components/device_tracker/locative.py | 20 ++--- .../components/device_tracker/meraki.py | 12 +-- .../components/device_tracker/mqtt.py | 6 +- .../components/device_tracker/mqtt_json.py | 6 +- .../components/device_tracker/owntracks.py | 78 +++++++---------- .../device_tracker/owntracks_http.py | 11 +-- .../components/device_tracker/upc_connect.py | 29 +++---- homeassistant/components/fan/dyson.py | 4 +- homeassistant/components/fan/insteon.py | 19 ++--- homeassistant/components/fan/wink.py | 4 +- homeassistant/components/hassio/__init__.py | 48 +++++------ homeassistant/components/hassio/handler.py | 8 +- homeassistant/components/hassio/http.py | 14 ++-- .../image_processing/microsoft_face_detect.py | 11 +-- .../microsoft_face_identify.py | 13 ++- .../image_processing/openalpr_cloud.py | 12 ++- .../image_processing/openalpr_local.py | 12 ++- .../image_processing/seven_segments.py | 6 +- homeassistant/components/input_number.py | 26 +++--- homeassistant/components/input_select.py | 26 +++--- homeassistant/components/input_text.py | 16 ++-- 23 files changed, 194 insertions(+), 286 deletions(-) diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index af1bb1cd9b5..b9cc72e5a5a 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -139,8 +139,7 @@ def see(hass: HomeAssistantType, mac: str = None, dev_id: str = None, hass.services.call(DOMAIN, SERVICE_SEE, data) -@asyncio.coroutine -def async_setup(hass: HomeAssistantType, config: ConfigType): +async def async_setup(hass: HomeAssistantType, config: ConfigType): """Set up the device tracker.""" yaml_path = hass.config.path(YAML_DEVICES) @@ -153,14 +152,13 @@ def async_setup(hass: HomeAssistantType, config: ConfigType): if track_new is None: track_new = defaults.get(CONF_TRACK_NEW, DEFAULT_TRACK_NEW) - devices = yield from async_load_config(yaml_path, hass, consider_home) + devices = await async_load_config(yaml_path, hass, consider_home) tracker = DeviceTracker( hass, consider_home, track_new, defaults, devices) - @asyncio.coroutine - def async_setup_platform(p_type, p_config, disc_info=None): + async def async_setup_platform(p_type, p_config, disc_info=None): """Set up a device tracker platform.""" - platform = yield from async_prepare_setup_platform( + platform = await async_prepare_setup_platform( hass, config, DOMAIN, p_type) if platform is None: return @@ -170,16 +168,16 @@ def async_setup(hass: HomeAssistantType, config: ConfigType): scanner = None setup = None if hasattr(platform, 'async_get_scanner'): - scanner = yield from platform.async_get_scanner( + scanner = await platform.async_get_scanner( hass, {DOMAIN: p_config}) elif hasattr(platform, 'get_scanner'): - scanner = yield from hass.async_add_job( + scanner = await hass.async_add_job( platform.get_scanner, hass, {DOMAIN: p_config}) elif hasattr(platform, 'async_setup_scanner'): - setup = yield from platform.async_setup_scanner( + setup = await platform.async_setup_scanner( hass, p_config, tracker.async_see, disc_info) elif hasattr(platform, 'setup_scanner'): - setup = yield from hass.async_add_job( + setup = await hass.async_add_job( platform.setup_scanner, hass, p_config, tracker.see, disc_info) else: @@ -200,14 +198,13 @@ def async_setup(hass: HomeAssistantType, config: ConfigType): setup_tasks = [async_setup_platform(p_type, p_config) for p_type, p_config in config_per_platform(config, DOMAIN)] if setup_tasks: - yield from asyncio.wait(setup_tasks, loop=hass.loop) + await asyncio.wait(setup_tasks, loop=hass.loop) tracker.async_setup_group() - @asyncio.coroutine - def async_platform_discovered(platform, info): + async def async_platform_discovered(platform, info): """Load a platform.""" - yield from async_setup_platform(platform, {}, disc_info=info) + await async_setup_platform(platform, {}, disc_info=info) discovery.async_listen_platform(hass, DOMAIN, async_platform_discovered) @@ -215,20 +212,19 @@ def async_setup(hass: HomeAssistantType, config: ConfigType): async_track_utc_time_change( hass, tracker.async_update_stale, second=range(0, 60, 5)) - @asyncio.coroutine - def async_see_service(call): + async def async_see_service(call): """Service to see a device.""" # Temp workaround for iOS, introduced in 0.65 data = dict(call.data) data.pop('hostname', None) data.pop('battery_status', None) - yield from tracker.async_see(**data) + await tracker.async_see(**data) hass.services.async_register( DOMAIN, SERVICE_SEE, async_see_service, SERVICE_SEE_PAYLOAD_SCHEMA) # restore - yield from tracker.async_setup_tracked_device() + await tracker.async_setup_tracked_device() return True @@ -269,8 +265,7 @@ class DeviceTracker: picture, icon, consider_home) ) - @asyncio.coroutine - def async_see( + async def async_see( self, mac: str = None, dev_id: str = None, host_name: str = None, location_name: str = None, gps: GPSType = None, gps_accuracy: int = None, battery: int = None, @@ -293,11 +288,11 @@ class DeviceTracker: device = self.devices.get(dev_id) if device: - yield from device.async_seen( + await device.async_seen( host_name, location_name, gps, gps_accuracy, battery, attributes, source_type, consider_home) if device.track: - yield from device.async_update_ha_state() + await device.async_update_ha_state() return # If no device can be found, create it @@ -311,12 +306,12 @@ class DeviceTracker: if mac is not None: self.mac_to_dev[mac] = device - yield from device.async_seen( + await device.async_seen( host_name, location_name, gps, gps_accuracy, battery, attributes, source_type) if device.track: - yield from device.async_update_ha_state() + await device.async_update_ha_state() # During init, we ignore the group if self.group and self.track_new: @@ -378,17 +373,15 @@ class DeviceTracker: device.stale(now): self.hass.async_add_job(device.async_update_ha_state(True)) - @asyncio.coroutine - def async_setup_tracked_device(self): + async def async_setup_tracked_device(self): """Set up all not exists tracked devices. This method is a coroutine. """ - @asyncio.coroutine - def async_init_single_device(dev): + async def async_init_single_device(dev): """Init a single device_tracker entity.""" - yield from dev.async_added_to_hass() - yield from dev.async_update_ha_state() + await dev.async_added_to_hass() + await dev.async_update_ha_state() tasks = [] for device in self.devices.values(): @@ -397,7 +390,7 @@ class DeviceTracker: async_init_single_device(device))) if tasks: - yield from asyncio.wait(tasks, loop=self.hass.loop) + await asyncio.wait(tasks, loop=self.hass.loop) class Device(Entity): @@ -495,12 +488,12 @@ class Device(Entity): """If device should be hidden.""" return self.away_hide and self.state != STATE_HOME - @asyncio.coroutine - def async_seen(self, host_name: str = None, location_name: str = None, - gps: GPSType = None, gps_accuracy=0, battery: int = None, - attributes: dict = None, - source_type: str = SOURCE_TYPE_GPS, - consider_home: timedelta = None): + async def async_seen( + self, host_name: str = None, location_name: str = None, + gps: GPSType = None, gps_accuracy=0, battery: int = None, + attributes: dict = None, + source_type: str = SOURCE_TYPE_GPS, + consider_home: timedelta = None): """Mark the device as seen.""" self.source_type = source_type self.last_seen = dt_util.utcnow() @@ -526,7 +519,7 @@ class Device(Entity): "Could not parse gps value for %s: %s", self.dev_id, gps) # pylint: disable=not-an-iterable - yield from self.async_update() + await self.async_update() def stale(self, now: dt_util.dt.datetime = None): """Return if device state is stale. @@ -536,8 +529,7 @@ class Device(Entity): return self.last_seen and \ (now or dt_util.utcnow()) - self.last_seen > self.consider_home - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Update state of entity. This method is a coroutine. @@ -563,10 +555,9 @@ class Device(Entity): self._state = STATE_HOME self.last_update_home = True - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Add an entity.""" - state = yield from async_get_last_state(self.hass, self.entity_id) + state = await async_get_last_state(self.hass, self.entity_id) if not state: return self._state = state.state @@ -629,9 +620,8 @@ def load_config(path: str, hass: HomeAssistantType, consider_home: timedelta): async_load_config(path, hass, consider_home), hass.loop).result() -@asyncio.coroutine -def async_load_config(path: str, hass: HomeAssistantType, - consider_home: timedelta): +async def async_load_config(path: str, hass: HomeAssistantType, + consider_home: timedelta): """Load devices from YAML configuration file. This method is a coroutine. @@ -651,7 +641,7 @@ def async_load_config(path: str, hass: HomeAssistantType, try: result = [] try: - devices = yield from hass.async_add_job( + devices = await hass.async_add_job( load_yaml_config_file, path) except HomeAssistantError as err: _LOGGER.error("Unable to load %s: %s", path, str(err)) diff --git a/homeassistant/components/device_tracker/geofency.py b/homeassistant/components/device_tracker/geofency.py index 7231c5127be..3687571c118 100644 --- a/homeassistant/components/device_tracker/geofency.py +++ b/homeassistant/components/device_tracker/geofency.py @@ -4,7 +4,6 @@ Support for the Geofency platform. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/device_tracker.geofency/ """ -import asyncio from functools import partial import logging @@ -58,10 +57,9 @@ class GeofencyView(HomeAssistantView): self.see = see self.mobile_beacons = [slugify(beacon) for beacon in mobile_beacons] - @asyncio.coroutine - def post(self, request): + async def post(self, request): """Handle Geofency requests.""" - data = yield from request.post() + data = await request.post() hass = request.app['hass'] data = self._validate_data(data) @@ -69,7 +67,7 @@ class GeofencyView(HomeAssistantView): return ("Invalid data", HTTP_UNPROCESSABLE_ENTITY) if self._is_mobile_beacon(data): - return (yield from self._set_location(hass, data, None)) + return await self._set_location(hass, data, None) if data['entry'] == LOCATION_ENTRY: location_name = data['name'] else: @@ -78,7 +76,7 @@ class GeofencyView(HomeAssistantView): data[ATTR_LATITUDE] = data[ATTR_CURRENT_LATITUDE] data[ATTR_LONGITUDE] = data[ATTR_CURRENT_LONGITUDE] - return (yield from self._set_location(hass, data, location_name)) + return await self._set_location(hass, data, location_name) @staticmethod def _validate_data(data): @@ -121,12 +119,11 @@ class GeofencyView(HomeAssistantView): return "{}_{}".format(BEACON_DEV_PREFIX, data['name']) return data['device'] - @asyncio.coroutine - def _set_location(self, hass, data, location_name): + async def _set_location(self, hass, data, location_name): """Fire HA event to set location.""" device = self._device_name(data) - yield from hass.async_add_job( + await hass.async_add_job( partial(self.see, dev_id=device, gps=(data[ATTR_LATITUDE], data[ATTR_LONGITUDE]), location_name=location_name, diff --git a/homeassistant/components/device_tracker/locative.py b/homeassistant/components/device_tracker/locative.py index 354d3b0980c..aa91f0d3d71 100644 --- a/homeassistant/components/device_tracker/locative.py +++ b/homeassistant/components/device_tracker/locative.py @@ -4,7 +4,6 @@ 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/ """ -import asyncio from functools import partial import logging @@ -38,21 +37,18 @@ class LocativeView(HomeAssistantView): """Initialize Locative URL endpoints.""" self.see = see - @asyncio.coroutine - def get(self, request): + async def get(self, request): """Locative message received as GET.""" - res = yield from self._handle(request.app['hass'], request.query) + res = await self._handle(request.app['hass'], request.query) return res - @asyncio.coroutine - def post(self, request): + async def post(self, request): """Locative message received.""" - data = yield from request.post() - res = yield from self._handle(request.app['hass'], data) + data = await request.post() + res = await self._handle(request.app['hass'], data) return res - @asyncio.coroutine - def _handle(self, 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.', @@ -79,7 +75,7 @@ class LocativeView(HomeAssistantView): gps_location = (data[ATTR_LATITUDE], data[ATTR_LONGITUDE]) if direction == 'enter': - yield from hass.async_add_job( + 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) @@ -90,7 +86,7 @@ class LocativeView(HomeAssistantView): if current_state is None or current_state.state == location_name: location_name = STATE_NOT_HOME - yield from hass.async_add_job( + await hass.async_add_job( partial(self.see, dev_id=device, location_name=location_name, gps=gps_location)) return 'Setting location to not home' diff --git a/homeassistant/components/device_tracker/meraki.py b/homeassistant/components/device_tracker/meraki.py index c996b7e643b..416f202ab98 100644 --- a/homeassistant/components/device_tracker/meraki.py +++ b/homeassistant/components/device_tracker/meraki.py @@ -5,7 +5,6 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/device_tracker.meraki/ """ -import asyncio import logging import json @@ -33,8 +32,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_scanner(hass, config, async_see, discovery_info=None): +async def async_setup_scanner(hass, config, async_see, discovery_info=None): """Set up an endpoint for the Meraki tracker.""" hass.http.register_view( MerakiView(config, async_see)) @@ -54,16 +52,14 @@ class MerakiView(HomeAssistantView): self.validator = config[CONF_VALIDATOR] self.secret = config[CONF_SECRET] - @asyncio.coroutine - def get(self, request): + async def get(self, request): """Meraki message received as GET.""" return self.validator - @asyncio.coroutine - def post(self, request): + async def post(self, request): """Meraki CMX message received.""" try: - data = yield from request.json() + data = await request.json() except ValueError: return self.json_message('Invalid JSON', HTTP_BAD_REQUEST) _LOGGER.debug("Meraki Data from Post: %s", json.dumps(data)) diff --git a/homeassistant/components/device_tracker/mqtt.py b/homeassistant/components/device_tracker/mqtt.py index b5031e8ccfb..3623abf31c3 100644 --- a/homeassistant/components/device_tracker/mqtt.py +++ b/homeassistant/components/device_tracker/mqtt.py @@ -4,7 +4,6 @@ Support for tracking MQTT enabled devices. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/device_tracker.mqtt/ """ -import asyncio import logging import voluptuous as vol @@ -25,8 +24,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(mqtt.SCHEMA_BASE).extend({ }) -@asyncio.coroutine -def async_setup_scanner(hass, config, async_see, discovery_info=None): +async def async_setup_scanner(hass, config, async_see, discovery_info=None): """Set up the MQTT tracker.""" devices = config[CONF_DEVICES] qos = config[CONF_QOS] @@ -38,7 +36,7 @@ def async_setup_scanner(hass, config, async_see, discovery_info=None): hass.async_add_job( async_see(dev_id=dev_id, location_name=payload)) - yield from mqtt.async_subscribe( + await mqtt.async_subscribe( hass, topic, async_message_received, qos) return True diff --git a/homeassistant/components/device_tracker/mqtt_json.py b/homeassistant/components/device_tracker/mqtt_json.py index 7e5ae7c9227..a95180f2eb2 100644 --- a/homeassistant/components/device_tracker/mqtt_json.py +++ b/homeassistant/components/device_tracker/mqtt_json.py @@ -4,7 +4,6 @@ Support for GPS tracking MQTT enabled devices. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/device_tracker.mqtt_json/ """ -import asyncio import json import logging @@ -35,8 +34,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(mqtt.SCHEMA_BASE).extend({ }) -@asyncio.coroutine -def async_setup_scanner(hass, config, async_see, discovery_info=None): +async def async_setup_scanner(hass, config, async_see, discovery_info=None): """Set up the MQTT JSON tracker.""" devices = config[CONF_DEVICES] qos = config[CONF_QOS] @@ -59,7 +57,7 @@ def async_setup_scanner(hass, config, async_see, discovery_info=None): kwargs = _parse_see_args(dev_id, data) hass.async_add_job(async_see(**kwargs)) - yield from mqtt.async_subscribe( + await mqtt.async_subscribe( hass, topic, async_message_received, qos) return True diff --git a/homeassistant/components/device_tracker/owntracks.py b/homeassistant/components/device_tracker/owntracks.py index 2d7f1e80406..10f71450f69 100644 --- a/homeassistant/components/device_tracker/owntracks.py +++ b/homeassistant/components/device_tracker/owntracks.py @@ -4,7 +4,6 @@ Device tracker platform that adds support for OwnTracks over MQTT. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/device_tracker.owntracks/ """ -import asyncio import base64 import json import logging @@ -73,13 +72,11 @@ def get_cipher(): return (KEYLEN, decrypt) -@asyncio.coroutine -def async_setup_scanner(hass, config, async_see, discovery_info=None): +async def async_setup_scanner(hass, config, async_see, discovery_info=None): """Set up an OwnTracks tracker.""" context = context_from_config(async_see, config) - @asyncio.coroutine - def async_handle_mqtt_message(topic, payload, qos): + async def async_handle_mqtt_message(topic, payload, qos): """Handle incoming OwnTracks message.""" try: message = json.loads(payload) @@ -90,9 +87,9 @@ def async_setup_scanner(hass, config, async_see, discovery_info=None): message['topic'] = topic - yield from async_handle_message(hass, context, message) + await async_handle_message(hass, context, message) - yield from mqtt.async_subscribe( + await mqtt.async_subscribe( hass, context.mqtt_topic, async_handle_mqtt_message, 1) return True @@ -266,8 +263,7 @@ class OwnTracksContext: return True - @asyncio.coroutine - def async_see_beacons(self, hass, dev_id, kwargs_param): + async def async_see_beacons(self, hass, dev_id, kwargs_param): """Set active beacons to the current location.""" kwargs = kwargs_param.copy() @@ -290,12 +286,11 @@ class OwnTracksContext: for beacon in self.mobile_beacons_active[dev_id]: kwargs['dev_id'] = "{}_{}".format(BEACON_DEV_ID, beacon) kwargs['host_name'] = beacon - yield from self.async_see(**kwargs) + await self.async_see(**kwargs) @HANDLERS.register('location') -@asyncio.coroutine -def async_handle_location_message(hass, context, message): +async def async_handle_location_message(hass, context, message): """Handle a location message.""" if not context.async_valid_accuracy(message): return @@ -312,12 +307,11 @@ def async_handle_location_message(hass, context, message): context.regions_entered[-1]) return - yield from context.async_see(**kwargs) - yield from context.async_see_beacons(hass, dev_id, kwargs) + await context.async_see(**kwargs) + await context.async_see_beacons(hass, dev_id, kwargs) -@asyncio.coroutine -def _async_transition_message_enter(hass, context, message, location): +async def _async_transition_message_enter(hass, context, message, location): """Execute enter event.""" zone = hass.states.get("zone.{}".format(slugify(location))) dev_id, kwargs = _parse_see_args(message, context.mqtt_topic) @@ -331,7 +325,7 @@ def _async_transition_message_enter(hass, context, message, location): if location not in beacons: beacons.add(location) _LOGGER.info("Added beacon %s", location) - yield from context.async_see_beacons(hass, dev_id, kwargs) + await context.async_see_beacons(hass, dev_id, kwargs) else: # Normal region regions = context.regions_entered[dev_id] @@ -339,12 +333,11 @@ def _async_transition_message_enter(hass, context, message, location): regions.append(location) _LOGGER.info("Enter region %s", location) _set_gps_from_zone(kwargs, location, zone) - yield from context.async_see(**kwargs) - yield from context.async_see_beacons(hass, dev_id, kwargs) + await context.async_see(**kwargs) + await context.async_see_beacons(hass, dev_id, kwargs) -@asyncio.coroutine -def _async_transition_message_leave(hass, context, message, location): +async def _async_transition_message_leave(hass, context, message, location): """Execute leave event.""" dev_id, kwargs = _parse_see_args(message, context.mqtt_topic) regions = context.regions_entered[dev_id] @@ -356,7 +349,7 @@ def _async_transition_message_leave(hass, context, message, location): if location in beacons: beacons.remove(location) _LOGGER.info("Remove beacon %s", location) - yield from context.async_see_beacons(hass, dev_id, kwargs) + await context.async_see_beacons(hass, dev_id, kwargs) else: new_region = regions[-1] if regions else None if new_region: @@ -365,21 +358,20 @@ def _async_transition_message_leave(hass, context, message, location): "zone.{}".format(slugify(new_region))) _set_gps_from_zone(kwargs, new_region, zone) _LOGGER.info("Exit to %s", new_region) - yield from context.async_see(**kwargs) - yield from context.async_see_beacons(hass, dev_id, kwargs) + await context.async_see(**kwargs) + await context.async_see_beacons(hass, dev_id, kwargs) return _LOGGER.info("Exit to GPS") # Check for GPS accuracy if context.async_valid_accuracy(message): - yield from context.async_see(**kwargs) - yield from context.async_see_beacons(hass, dev_id, kwargs) + await context.async_see(**kwargs) + await context.async_see_beacons(hass, dev_id, kwargs) @HANDLERS.register('transition') -@asyncio.coroutine -def async_handle_transition_message(hass, context, message): +async def async_handle_transition_message(hass, context, message): """Handle a transition message.""" if message.get('desc') is None: _LOGGER.error( @@ -399,10 +391,10 @@ def async_handle_transition_message(hass, context, message): location = STATE_HOME if message['event'] == 'enter': - yield from _async_transition_message_enter( + await _async_transition_message_enter( hass, context, message, location) elif message['event'] == 'leave': - yield from _async_transition_message_leave( + await _async_transition_message_leave( hass, context, message, location) else: _LOGGER.error( @@ -410,8 +402,7 @@ def async_handle_transition_message(hass, context, message): message['event']) -@asyncio.coroutine -def async_handle_waypoint(hass, name_base, waypoint): +async def async_handle_waypoint(hass, name_base, waypoint): """Handle a waypoint.""" name = waypoint['desc'] pretty_name = '{} - {}'.format(name_base, name) @@ -429,13 +420,12 @@ def async_handle_waypoint(hass, name_base, waypoint): zone = zone_comp.Zone(hass, pretty_name, lat, lon, rad, zone_comp.ICON_IMPORT, False) zone.entity_id = entity_id - yield from zone.async_update_ha_state() + await zone.async_update_ha_state() @HANDLERS.register('waypoint') @HANDLERS.register('waypoints') -@asyncio.coroutine -def async_handle_waypoints_message(hass, context, message): +async def async_handle_waypoints_message(hass, context, message): """Handle a waypoints message.""" if not context.import_waypoints: return @@ -456,12 +446,11 @@ def async_handle_waypoints_message(hass, context, message): name_base = ' '.join(_parse_topic(message['topic'], context.mqtt_topic)) for wayp in wayps: - yield from async_handle_waypoint(hass, name_base, wayp) + await async_handle_waypoint(hass, name_base, wayp) @HANDLERS.register('encrypted') -@asyncio.coroutine -def async_handle_encrypted_message(hass, context, message): +async def async_handle_encrypted_message(hass, context, message): """Handle an encrypted message.""" plaintext_payload = _decrypt_payload(context.secret, message['topic'], message['data']) @@ -472,7 +461,7 @@ def async_handle_encrypted_message(hass, context, message): decrypted = json.loads(plaintext_payload) decrypted['topic'] = message['topic'] - yield from async_handle_message(hass, context, decrypted) + await async_handle_message(hass, context, decrypted) @HANDLERS.register('lwt') @@ -481,24 +470,21 @@ def async_handle_encrypted_message(hass, context, message): @HANDLERS.register('cmd') @HANDLERS.register('steps') @HANDLERS.register('card') -@asyncio.coroutine -def async_handle_not_impl_msg(hass, context, message): +async def async_handle_not_impl_msg(hass, context, message): """Handle valid but not implemented message types.""" _LOGGER.debug('Not handling %s message: %s', message.get("_type"), message) -@asyncio.coroutine -def async_handle_unsupported_msg(hass, context, message): +async def async_handle_unsupported_msg(hass, context, message): """Handle an unsupported or invalid message type.""" _LOGGER.warning('Received unsupported message type: %s.', message.get('_type')) -@asyncio.coroutine -def async_handle_message(hass, context, message): +async def async_handle_message(hass, context, message): """Handle an OwnTracks message.""" msgtype = message.get('_type') handler = HANDLERS.get(msgtype, async_handle_unsupported_msg) - yield from handler(hass, context, message) + await handler(hass, context, message) diff --git a/homeassistant/components/device_tracker/owntracks_http.py b/homeassistant/components/device_tracker/owntracks_http.py index d74e1fc6d95..b9a813738ad 100644 --- a/homeassistant/components/device_tracker/owntracks_http.py +++ b/homeassistant/components/device_tracker/owntracks_http.py @@ -4,7 +4,6 @@ Device tracker platform that adds support for OwnTracks over HTTP. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/device_tracker.owntracks_http/ """ -import asyncio import re from aiohttp.web_exceptions import HTTPInternalServerError @@ -19,8 +18,7 @@ from .owntracks import ( # NOQA DEPENDENCIES = ['http'] -@asyncio.coroutine -def async_setup_scanner(hass, config, async_see, discovery_info=None): +async def async_setup_scanner(hass, config, async_see, discovery_info=None): """Set up an OwnTracks tracker.""" context = context_from_config(async_see, config) @@ -39,19 +37,18 @@ class OwnTracksView(HomeAssistantView): """Initialize OwnTracks URL endpoints.""" self.context = context - @asyncio.coroutine - def post(self, request, user, device): + async def post(self, request, user, device): """Handle an OwnTracks message.""" hass = request.app['hass'] subscription = self.context.mqtt_topic topic = re.sub('/#$', '', subscription) - message = yield from request.json() + message = await request.json() message['topic'] = '{}/{}/{}'.format(topic, user, device) try: - yield from async_handle_message(hass, self.context, message) + await async_handle_message(hass, self.context, message) return self.json([]) except ValueError: diff --git a/homeassistant/components/device_tracker/upc_connect.py b/homeassistant/components/device_tracker/upc_connect.py index ea0645e012f..2ee6d64730d 100644 --- a/homeassistant/components/device_tracker/upc_connect.py +++ b/homeassistant/components/device_tracker/upc_connect.py @@ -31,11 +31,10 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_get_scanner(hass, config): +async def async_get_scanner(hass, config): """Return the UPC device scanner.""" scanner = UPCDeviceScanner(hass, config[DOMAIN]) - success_init = yield from scanner.async_initialize_token() + success_init = await scanner.async_initialize_token() return scanner if success_init else None @@ -61,18 +60,17 @@ class UPCDeviceScanner(DeviceScanner): self.websession = async_get_clientsession(hass) - @asyncio.coroutine - def async_scan_devices(self): + async def async_scan_devices(self): """Scan for new devices and return a list with found device IDs.""" import defusedxml.ElementTree as ET if self.token is None: - token_initialized = yield from self.async_initialize_token() + token_initialized = await self.async_initialize_token() if not token_initialized: _LOGGER.error("Not connected to %s", self.host) return [] - raw = yield from self._async_ws_function(CMD_DEVICES) + raw = await self._async_ws_function(CMD_DEVICES) try: xml_root = ET.fromstring(raw) @@ -82,22 +80,20 @@ class UPCDeviceScanner(DeviceScanner): self.token = None return [] - @asyncio.coroutine - def async_get_device_name(self, device): + async def async_get_device_name(self, device): """Get the device name (the name of the wireless device not used).""" return None - @asyncio.coroutine - def async_initialize_token(self): + async def async_initialize_token(self): """Get first token.""" try: # get first token with async_timeout.timeout(10, loop=self.hass.loop): - response = yield from self.websession.get( + response = await self.websession.get( "http://{}/common_page/login.html".format(self.host), headers=self.headers) - yield from response.text() + await response.text() self.token = response.cookies['sessionToken'].value @@ -107,14 +103,13 @@ class UPCDeviceScanner(DeviceScanner): _LOGGER.error("Can not load login page from %s", self.host) return False - @asyncio.coroutine - def _async_ws_function(self, function): + async def _async_ws_function(self, function): """Execute a command on UPC firmware webservice.""" try: with async_timeout.timeout(10, loop=self.hass.loop): # The 'token' parameter has to be first, and 'fun' second # or the UPC firmware will return an error - response = yield from self.websession.post( + response = await self.websession.post( "http://{}/xml/getter.xml".format(self.host), data="token={}&fun={}".format(self.token, function), headers=self.headers, allow_redirects=False) @@ -127,7 +122,7 @@ class UPCDeviceScanner(DeviceScanner): # Load data, store token for next request self.token = response.cookies['sessionToken'].value - return (yield from response.text()) + return await response.text() except (asyncio.TimeoutError, aiohttp.ClientError): _LOGGER.error("Error on %s", function) diff --git a/homeassistant/components/fan/dyson.py b/homeassistant/components/fan/dyson.py index 9f505c87b3d..ef517021178 100644 --- a/homeassistant/components/fan/dyson.py +++ b/homeassistant/components/fan/dyson.py @@ -3,7 +3,6 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/fan.dyson/ """ -import asyncio import logging import voluptuous as vol @@ -77,8 +76,7 @@ class DysonPureCoolLinkDevice(FanEntity): self.hass = hass self._device = device - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Call when entity is added to hass.""" self.hass.async_add_job( self._device.add_message_listener, self.on_message) diff --git a/homeassistant/components/fan/insteon.py b/homeassistant/components/fan/insteon.py index f938ae7aec1..604063a9aa3 100644 --- a/homeassistant/components/fan/insteon.py +++ b/homeassistant/components/fan/insteon.py @@ -4,7 +4,6 @@ Support for INSTEON fans via PowerLinc Modem. For more details about this component, please refer to the documentation at https://home-assistant.io/components/fan.insteon/ """ -import asyncio import logging from homeassistant.components.fan import (SPEED_OFF, @@ -28,9 +27,8 @@ FAN_SPEEDS = [STATE_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH] _LOGGER = logging.getLogger(__name__) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the INSTEON device class for the hass platform.""" insteon_modem = hass.data['insteon'].get('modem') @@ -64,20 +62,17 @@ class InsteonFan(InsteonEntity, FanEntity): """Flag supported features.""" return SUPPORT_SET_SPEED - @asyncio.coroutine - def async_turn_on(self, speed: str = None, **kwargs) -> None: + async def async_turn_on(self, speed: str = None, **kwargs) -> None: """Turn on the entity.""" if speed is None: speed = SPEED_MEDIUM - yield from self.async_set_speed(speed) + await self.async_set_speed(speed) - @asyncio.coroutine - def async_turn_off(self, **kwargs) -> None: + async def async_turn_off(self, **kwargs) -> None: """Turn off the entity.""" - yield from self.async_set_speed(SPEED_OFF) + await self.async_set_speed(SPEED_OFF) - @asyncio.coroutine - def async_set_speed(self, speed: str) -> None: + async def async_set_speed(self, speed: str) -> None: """Set the speed of the fan.""" fan_speed = SPEED_TO_HEX[speed] if fan_speed == 0x00: diff --git a/homeassistant/components/fan/wink.py b/homeassistant/components/fan/wink.py index 480801c48c0..d0dc386d74d 100644 --- a/homeassistant/components/fan/wink.py +++ b/homeassistant/components/fan/wink.py @@ -4,7 +4,6 @@ Support for Wink fans. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/fan.wink/ """ -import asyncio import logging from homeassistant.components.fan import ( @@ -33,8 +32,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class WinkFanDevice(WinkDevice, FanEntity): """Representation of a Wink fan.""" - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Call when entity is added to hass.""" self.hass.data[DOMAIN]['entities']['fan'].append(self) diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index e0356017e3e..a0c603b018f 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -4,7 +4,6 @@ Exposes regular REST commands as services. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/hassio/ """ -import asyncio from datetime import timedelta import logging import os @@ -134,11 +133,10 @@ def is_hassio(hass): @bind_hass -@asyncio.coroutine -def async_check_config(hass): +async def async_check_config(hass): """Check configuration over Hass.io API.""" hassio = hass.data[DOMAIN] - result = yield from hassio.check_homeassistant_config() + result = await hassio.check_homeassistant_config() if not result: return "Hass.io config check API error" @@ -147,8 +145,7 @@ def async_check_config(hass): return None -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Set up the Hass.io component.""" try: host = os.environ['HASSIO'] @@ -165,27 +162,27 @@ def async_setup(hass, config): websession = hass.helpers.aiohttp_client.async_get_clientsession() hass.data[DOMAIN] = hassio = HassIO(hass.loop, websession, host) - if not (yield from hassio.is_connected()): + if not await hassio.is_connected(): _LOGGER.error("Not connected with Hass.io") return False store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) - data = yield from store.async_load() + data = await store.async_load() if data is None: data = {} refresh_token = None if 'hassio_user' in data: - user = yield from hass.auth.async_get_user(data['hassio_user']) + user = await hass.auth.async_get_user(data['hassio_user']) if user and user.refresh_tokens: refresh_token = list(user.refresh_tokens.values())[0] if refresh_token is None: - user = yield from hass.auth.async_create_system_user('Hass.io') - refresh_token = yield from hass.auth.async_create_refresh_token(user) + user = await hass.auth.async_create_system_user('Hass.io') + refresh_token = await hass.auth.async_create_refresh_token(user) data['hassio_user'] = user.id - yield from store.async_save(data) + await store.async_save(data) # This overrides the normal API call that would be forwarded development_repo = config.get(DOMAIN, {}).get(CONF_FRONTEND_REPO) @@ -197,7 +194,7 @@ def async_setup(hass, config): hass.http.register_view(HassIOView(host, websession)) if 'frontend' in hass.config.components: - yield from hass.components.panel_custom.async_register_panel( + await hass.components.panel_custom.async_register_panel( frontend_url_path='hassio', webcomponent_name='hassio-main', sidebar_title='Hass.io', @@ -212,13 +209,12 @@ def async_setup(hass, config): else: token = None - yield from hassio.update_hass_api(config.get('http', {}), token) + await hassio.update_hass_api(config.get('http', {}), token) if 'homeassistant' in config: - yield from hassio.update_hass_timezone(config['homeassistant']) + await hassio.update_hass_timezone(config['homeassistant']) - @asyncio.coroutine - def async_service_handler(service): + async def async_service_handler(service): """Handle service calls for Hass.io.""" api_command = MAP_SERVICE_API[service.service][0] data = service.data.copy() @@ -233,7 +229,7 @@ def async_setup(hass, config): payload = data # Call API - ret = yield from hassio.send_command( + ret = await hassio.send_command( api_command.format(addon=addon, snapshot=snapshot), payload=payload, timeout=MAP_SERVICE_API[service.service][2] ) @@ -245,10 +241,9 @@ def async_setup(hass, config): hass.services.async_register( DOMAIN, service, async_service_handler, schema=settings[1]) - @asyncio.coroutine - def update_homeassistant_version(now): + async def update_homeassistant_version(now): """Update last available Home Assistant version.""" - data = yield from hassio.get_homeassistant_info() + data = await hassio.get_homeassistant_info() if data: hass.data[DATA_HOMEASSISTANT_VERSION] = data['last_version'] @@ -256,16 +251,15 @@ def async_setup(hass, config): update_homeassistant_version, utcnow() + HASSIO_UPDATE_INTERVAL) # Fetch last version - yield from update_homeassistant_version(None) + await update_homeassistant_version(None) - @asyncio.coroutine - def async_handle_core_service(call): + async def async_handle_core_service(call): """Service handler for handling core services.""" if call.service == SERVICE_HOMEASSISTANT_STOP: - yield from hassio.stop_homeassistant() + await hassio.stop_homeassistant() return - error = yield from async_check_config(hass) + error = await async_check_config(hass) if error: _LOGGER.error(error) hass.components.persistent_notification.async_create( @@ -274,7 +268,7 @@ def async_setup(hass, config): return if call.service == SERVICE_HOMEASSISTANT_RESTART: - yield from hassio.restart_homeassistant() + await hassio.restart_homeassistant() # Mock core services for service in (SERVICE_HOMEASSISTANT_STOP, SERVICE_HOMEASSISTANT_RESTART, diff --git a/homeassistant/components/hassio/handler.py b/homeassistant/components/hassio/handler.py index d75529a99b0..bbf675ee47a 100644 --- a/homeassistant/components/hassio/handler.py +++ b/homeassistant/components/hassio/handler.py @@ -120,15 +120,15 @@ class HassIO: 'timezone': core_config.get(CONF_TIME_ZONE) }) - @asyncio.coroutine - def send_command(self, command, method="post", payload=None, timeout=10): + async def send_command(self, command, method="post", payload=None, + timeout=10): """Send API command to Hass.io. This method is a coroutine. """ try: with async_timeout.timeout(timeout, loop=self.loop): - request = yield from self.websession.request( + request = await self.websession.request( method, "http://{}{}".format(self._ip, command), json=payload, headers={ X_HASSIO: os.environ.get('HASSIO_TOKEN', "") @@ -139,7 +139,7 @@ class HassIO: "%s return code %d.", command, request.status) return None - answer = yield from request.json() + answer = await request.json() return answer except asyncio.TimeoutError: diff --git a/homeassistant/components/hassio/http.py b/homeassistant/components/hassio/http.py index 55cc7f54787..abd1c16ba8b 100644 --- a/homeassistant/components/hassio/http.py +++ b/homeassistant/components/hassio/http.py @@ -54,15 +54,14 @@ class HassIOView(HomeAssistantView): self._host = host self._websession = websession - @asyncio.coroutine - def _handle(self, request, path): + async def _handle(self, request, path): """Route data to Hass.io.""" if _need_auth(path) and not request[KEY_AUTHENTICATED]: return web.Response(status=401) - client = yield from self._command_proxy(path, request) + client = await self._command_proxy(path, request) - data = yield from client.read() + data = await client.read() if path.endswith('/logs'): return _create_response_log(client, data) return _create_response(client, data) @@ -70,8 +69,7 @@ class HassIOView(HomeAssistantView): get = _handle post = _handle - @asyncio.coroutine - def _command_proxy(self, path, request): + async def _command_proxy(self, path, request): """Return a client request with proxy origin for Hass.io supervisor. This method is a coroutine. @@ -83,14 +81,14 @@ class HassIOView(HomeAssistantView): data = None headers = {X_HASSIO: os.environ.get('HASSIO_TOKEN', "")} with async_timeout.timeout(10, loop=hass.loop): - data = yield from request.read() + data = await request.read() if data: headers[CONTENT_TYPE] = request.content_type else: data = None method = getattr(self._websession, request.method.lower()) - client = yield from method( + client = await method( "http://{}/{}".format(self._host, path), data=data, headers=headers, timeout=read_timeout ) diff --git a/homeassistant/components/image_processing/microsoft_face_detect.py b/homeassistant/components/image_processing/microsoft_face_detect.py index 7e10d05c5b6..69bd8a8f931 100644 --- a/homeassistant/components/image_processing/microsoft_face_detect.py +++ b/homeassistant/components/image_processing/microsoft_face_detect.py @@ -4,7 +4,6 @@ Component that will help set the Microsoft face detect processing. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/image_processing.microsoft_face_detect/ """ -import asyncio import logging import voluptuous as vol @@ -45,9 +44,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the Microsoft Face detection platform.""" api = hass.data[DATA_MICROSOFT_FACE] attributes = config[CONF_ATTRIBUTES] @@ -88,15 +86,14 @@ class MicrosoftFaceDetectEntity(ImageProcessingFaceEntity): """Return the name of the entity.""" return self._name - @asyncio.coroutine - def async_process_image(self, image): + async def async_process_image(self, image): """Process image. This method is a coroutine. """ face_data = None try: - face_data = yield from self._api.call_api( + face_data = await self._api.call_api( 'post', 'detect', image, binary=True, params={'returnFaceAttributes': ",".join(self._attributes)}) diff --git a/homeassistant/components/image_processing/microsoft_face_identify.py b/homeassistant/components/image_processing/microsoft_face_identify.py index fae11a3dfa9..0a5b7725260 100644 --- a/homeassistant/components/image_processing/microsoft_face_identify.py +++ b/homeassistant/components/image_processing/microsoft_face_identify.py @@ -4,7 +4,6 @@ Component that will help set the Microsoft face for verify processing. For more details about this component, please refer to the documentation at https://home-assistant.io/components/image_processing.microsoft_face_identify/ """ -import asyncio import logging import voluptuous as vol @@ -29,9 +28,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the Microsoft Face identify platform.""" api = hass.data[DATA_MICROSOFT_FACE] face_group = config[CONF_GROUP] @@ -80,22 +78,21 @@ class MicrosoftFaceIdentifyEntity(ImageProcessingFaceEntity): """Return the name of the entity.""" return self._name - @asyncio.coroutine - def async_process_image(self, image): + async def async_process_image(self, image): """Process image. This method is a coroutine. """ detect = None try: - face_data = yield from self._api.call_api( + face_data = await self._api.call_api( 'post', 'detect', image, binary=True) if not face_data: return face_ids = [data['faceId'] for data in face_data] - detect = yield from self._api.call_api( + detect = await self._api.call_api( 'post', 'identify', {'faceIds': face_ids, 'personGroupId': self._face_group}) diff --git a/homeassistant/components/image_processing/openalpr_cloud.py b/homeassistant/components/image_processing/openalpr_cloud.py index 3daaeb6fa01..dea55b76025 100644 --- a/homeassistant/components/image_processing/openalpr_cloud.py +++ b/homeassistant/components/image_processing/openalpr_cloud.py @@ -48,9 +48,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the OpenALPR cloud API platform.""" confidence = config[CONF_CONFIDENCE] params = { @@ -101,8 +100,7 @@ class OpenAlprCloudEntity(ImageProcessingAlprEntity): """Return the name of the entity.""" return self._name - @asyncio.coroutine - def async_process_image(self, image): + async def async_process_image(self, image): """Process image. This method is a coroutine. @@ -116,11 +114,11 @@ class OpenAlprCloudEntity(ImageProcessingAlprEntity): try: with async_timeout.timeout(self.timeout, loop=self.hass.loop): - request = yield from websession.post( + request = await websession.post( OPENALPR_API_URL, params=params, data=body ) - data = yield from request.json() + data = await request.json() if request.status != 200: _LOGGER.error("Error %d -> %s.", diff --git a/homeassistant/components/image_processing/openalpr_local.py b/homeassistant/components/image_processing/openalpr_local.py index 901533d1da4..9d5ebf2e2b9 100644 --- a/homeassistant/components/image_processing/openalpr_local.py +++ b/homeassistant/components/image_processing/openalpr_local.py @@ -55,9 +55,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the OpenALPR local platform.""" command = [config[CONF_ALPR_BIN], '-c', config[CONF_REGION], '-'] confidence = config[CONF_CONFIDENCE] @@ -173,8 +172,7 @@ class OpenAlprLocalEntity(ImageProcessingAlprEntity): """Return the name of the entity.""" return self._name - @asyncio.coroutine - def async_process_image(self, image): + async def async_process_image(self, image): """Process image. This method is a coroutine. @@ -182,7 +180,7 @@ class OpenAlprLocalEntity(ImageProcessingAlprEntity): result = {} vehicles = 0 - alpr = yield from asyncio.create_subprocess_exec( + alpr = await asyncio.create_subprocess_exec( *self._cmd, loop=self.hass.loop, stdin=asyncio.subprocess.PIPE, @@ -191,7 +189,7 @@ class OpenAlprLocalEntity(ImageProcessingAlprEntity): ) # Send image - stdout, _ = yield from alpr.communicate(input=image) + stdout, _ = await alpr.communicate(input=image) stdout = io.StringIO(str(stdout, 'utf-8')) while True: diff --git a/homeassistant/components/image_processing/seven_segments.py b/homeassistant/components/image_processing/seven_segments.py index fb6f41b4a49..1f56ba6b572 100644 --- a/homeassistant/components/image_processing/seven_segments.py +++ b/homeassistant/components/image_processing/seven_segments.py @@ -4,7 +4,6 @@ Local optical character recognition processing of seven segments displays. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/image_processing.seven_segments/ """ -import asyncio import logging import io import os @@ -44,9 +43,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the Seven segments OCR platform.""" entities = [] for camera in config[CONF_SOURCE]: diff --git a/homeassistant/components/input_number.py b/homeassistant/components/input_number.py index 2eb811e1fb0..9630e943bf4 100644 --- a/homeassistant/components/input_number.py +++ b/homeassistant/components/input_number.py @@ -4,7 +4,6 @@ Component to offer a way to set a numeric value from a slider or text box. For more details about this component, please refer to the documentation at https://home-assistant.io/components/input_number/ """ -import asyncio import logging import voluptuous as vol @@ -81,8 +80,7 @@ CONFIG_SCHEMA = vol.Schema({ }, required=True, extra=vol.ALLOW_EXTRA) -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Set up an input slider.""" component = EntityComponent(_LOGGER, DOMAIN, hass) @@ -120,7 +118,7 @@ def async_setup(hass, config): 'async_decrement' ) - yield from component.async_add_entities(entities) + await component.async_add_entities(entities) return True @@ -175,13 +173,12 @@ class InputNumber(Entity): ATTR_MODE: self._mode, } - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Run when entity about to be added to hass.""" if self._current_value is not None: return - state = yield from async_get_last_state(self.hass, self.entity_id) + state = await async_get_last_state(self.hass, self.entity_id) value = state and float(state.state) # Check against None because value can be 0 @@ -190,8 +187,7 @@ class InputNumber(Entity): else: self._current_value = self._minimum - @asyncio.coroutine - def async_set_value(self, value): + async def async_set_value(self, value): """Set new value.""" num_value = float(value) if num_value < self._minimum or num_value > self._maximum: @@ -199,10 +195,9 @@ class InputNumber(Entity): num_value, self._minimum, self._maximum) return self._current_value = num_value - yield from self.async_update_ha_state() + await self.async_update_ha_state() - @asyncio.coroutine - def async_increment(self): + async def async_increment(self): """Increment value.""" new_value = self._current_value + self._step if new_value > self._maximum: @@ -210,10 +205,9 @@ class InputNumber(Entity): new_value, self._minimum, self._maximum) return self._current_value = new_value - yield from self.async_update_ha_state() + await self.async_update_ha_state() - @asyncio.coroutine - def async_decrement(self): + async def async_decrement(self): """Decrement value.""" new_value = self._current_value - self._step if new_value < self._minimum: @@ -221,4 +215,4 @@ class InputNumber(Entity): new_value, self._minimum, self._maximum) return self._current_value = new_value - yield from self.async_update_ha_state() + await self.async_update_ha_state() diff --git a/homeassistant/components/input_select.py b/homeassistant/components/input_select.py index 51175efecbd..b8398e1be3d 100644 --- a/homeassistant/components/input_select.py +++ b/homeassistant/components/input_select.py @@ -4,7 +4,6 @@ Component to offer a way to select an option from a list. For more details about this component, please refer to the documentation at https://home-assistant.io/components/input_select/ """ -import asyncio import logging import voluptuous as vol @@ -77,8 +76,7 @@ CONFIG_SCHEMA = vol.Schema({ }, required=True, extra=vol.ALLOW_EXTRA) -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Set up an input select.""" component = EntityComponent(_LOGGER, DOMAIN, hass) @@ -114,7 +112,7 @@ def async_setup(hass, config): 'async_set_options' ) - yield from component.async_add_entities(entities) + await component.async_add_entities(entities) return True @@ -129,13 +127,12 @@ class InputSelect(Entity): self._options = options self._icon = icon - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Run when entity about to be added.""" if self._current_option is not None: return - state = yield from async_get_last_state(self.hass, self.entity_id) + state = await async_get_last_state(self.hass, self.entity_id) if not state or state.state not in self._options: self._current_option = self._options[0] else: @@ -168,27 +165,24 @@ class InputSelect(Entity): ATTR_OPTIONS: self._options, } - @asyncio.coroutine - def async_select_option(self, option): + async def async_select_option(self, option): """Select new option.""" if option not in self._options: _LOGGER.warning('Invalid option: %s (possible options: %s)', option, ', '.join(self._options)) return self._current_option = option - yield from self.async_update_ha_state() + await self.async_update_ha_state() - @asyncio.coroutine - def async_offset_index(self, offset): + async def async_offset_index(self, offset): """Offset current index.""" current_index = self._options.index(self._current_option) new_index = (current_index + offset) % len(self._options) self._current_option = self._options[new_index] - yield from self.async_update_ha_state() + await self.async_update_ha_state() - @asyncio.coroutine - def async_set_options(self, options): + async def async_set_options(self, options): """Set options.""" self._current_option = options[0] self._options = options - yield from self.async_update_ha_state() + await self.async_update_ha_state() diff --git a/homeassistant/components/input_text.py b/homeassistant/components/input_text.py index fcc2352f523..956d9a6466d 100644 --- a/homeassistant/components/input_text.py +++ b/homeassistant/components/input_text.py @@ -4,7 +4,6 @@ Component to offer a way to enter a value into a text box. For more details about this component, please refer to the documentation at https://home-assistant.io/components/input_text/ """ -import asyncio import logging import voluptuous as vol @@ -73,8 +72,7 @@ CONFIG_SCHEMA = vol.Schema({ }, required=True, extra=vol.ALLOW_EXTRA) -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Set up an input text box.""" component = EntityComponent(_LOGGER, DOMAIN, hass) @@ -102,7 +100,7 @@ def async_setup(hass, config): 'async_set_value' ) - yield from component.async_add_entities(entities) + await component.async_add_entities(entities) return True @@ -157,25 +155,23 @@ class InputText(Entity): ATTR_MODE: self._mode, } - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Run when entity about to be added to hass.""" if self._current_value is not None: return - state = yield from async_get_last_state(self.hass, self.entity_id) + state = await async_get_last_state(self.hass, self.entity_id) value = state and state.state # Check against None because value can be 0 if value is not None and self._minimum <= len(value) <= self._maximum: self._current_value = value - @asyncio.coroutine - def async_set_value(self, value): + async def async_set_value(self, value): """Select new value.""" if len(value) < self._minimum or len(value) > self._maximum: _LOGGER.warning("Invalid value: %s (length range %s - %s)", value, self._minimum, self._maximum) return self._current_value = value - yield from self.async_update_ha_state() + await self.async_update_ha_state() From 760f822dce5091d6af8a2121f2099cbfa36fc5ea Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Mon, 1 Oct 2018 09:01:40 +0200 Subject: [PATCH 125/247] Fix MQTT discovery (#17004) --- homeassistant/components/mqtt/discovery.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py index 6a0b8555ddb..fdb7948e4bf 100644 --- a/homeassistant/components/mqtt/discovery.py +++ b/homeassistant/components/mqtt/discovery.py @@ -120,9 +120,9 @@ async def async_start(hass: HomeAssistantType, discovery_topic, hass_config, config_entries_key = '{}.{}'.format(component, platform) if config_entries_key not in hass.data[CONFIG_ENTRY_IS_SETUP]: + hass.data[CONFIG_ENTRY_IS_SETUP].add(config_entries_key) await hass.config_entries.async_forward_entry_setup( config_entry, component) - hass.data[CONFIG_ENTRY_IS_SETUP].add(config_entries_key) async_dispatcher_send(hass, MQTT_DISCOVERY_NEW.format( component, platform), payload) From d4cde2bfbfb846780109a6c2aa816f8e7307476d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Mon, 1 Oct 2018 11:52:49 +0300 Subject: [PATCH 126/247] Upgrade huawei-lte-api to 1.0.16 (#16972) --- 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 33da6be56db..ad134d8c60e 100644 --- a/homeassistant/components/huawei_lte.py +++ b/homeassistant/components/huawei_lte.py @@ -21,7 +21,7 @@ from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['huawei-lte-api==1.0.12'] +REQUIREMENTS = ['huawei-lte-api==1.0.16'] MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=10) diff --git a/requirements_all.txt b/requirements_all.txt index c56425a9671..3f4ec58cb92 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -468,7 +468,7 @@ homematicip==0.9.8 httplib2==0.10.3 # homeassistant.components.huawei_lte -huawei-lte-api==1.0.12 +huawei-lte-api==1.0.16 # homeassistant.components.hydrawise hydrawiser==0.1.1 From 6159f8b0ce1489eaba98bf2918bc5f430d8ff011 Mon Sep 17 00:00:00 2001 From: Rohan Kapoor Date: Mon, 1 Oct 2018 01:53:20 -0700 Subject: [PATCH 127/247] Fix switch.zoneminder name (#17026) --- homeassistant/components/switch/zoneminder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/switch/zoneminder.py b/homeassistant/components/switch/zoneminder.py index 265f94fbbb1..c28fe843b90 100644 --- a/homeassistant/components/switch/zoneminder.py +++ b/homeassistant/components/switch/zoneminder.py @@ -57,7 +57,7 @@ class ZMSwitchMonitors(SwitchDevice): @property def name(self): """Return the name of the switch.""" - return '{}\'s State'.format(self._monitor.name) + return '{} State'.format(self._monitor.name) def update(self): """Update the switch value.""" From 9edf1e515149df54c8c4a2d84c8dd93520df3e42 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Mon, 1 Oct 2018 10:55:16 +0200 Subject: [PATCH 128/247] Upgrade locationsharinglib to 3.0.3 (#17010) * Upgrade locationsharinglib to 3.0.3 * Revert change from #16969 --- homeassistant/components/device_tracker/google_maps.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 3 --- script/gen_requirements_all.py | 1 - 4 files changed, 2 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/device_tracker/google_maps.py b/homeassistant/components/device_tracker/google_maps.py index c0dcd2f00a7..77f499dcf6b 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.2'] +REQUIREMENTS = ['locationsharinglib==3.0.3'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 3f4ec58cb92..853e404c8b1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -558,7 +558,7 @@ liveboxplaytv==2.0.2 lmnotify==0.0.4 # homeassistant.components.device_tracker.google_maps -locationsharinglib==3.0.2 +locationsharinglib==3.0.3 # homeassistant.components.logi_circle logi_circle==0.1.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7c3ace8e8cf..04cec3b1e6f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -103,9 +103,6 @@ libpurecoollink==0.4.2 # homeassistant.components.media_player.soundtouch libsoundtouch==0.7.2 -# homeassistant.components.device_tracker.google_maps -locationsharinglib==3.0.2 - # homeassistant.components.sensor.mfi # homeassistant.components.switch.mfi mficlient==0.3.0 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 3fca95e1adf..7493e523273 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -63,7 +63,6 @@ TEST_REQUIREMENTS = ( 'influxdb', 'libpurecoollink', 'libsoundtouch', - 'locationsharinglib', 'mficlient', 'numpy', 'paho-mqtt', From 22a80cf733ac98e1a0216ad5bfe9243fd02fa6cb Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 1 Oct 2018 11:21:00 +0200 Subject: [PATCH 129/247] Break up websocket component (#17003) * Break up websocket component * Lint --- homeassistant/components/camera/__init__.py | 30 +- homeassistant/components/config/auth.py | 7 +- .../config/auth_provider_homeassistant.py | 5 +- .../components/config/entity_registry.py | 110 ++-- .../components/media_player/__init__.py | 31 +- .../__init__.py} | 357 +---------- .../components/websocket_api/commands.py | 183 ++++++ .../components/websocket_api/const.py | 8 + .../components/websocket_api/decorators.py | 101 ++++ .../components/websocket_api/messages.py | 42 ++ tests/components/camera/test_init.py | 5 +- tests/components/frontend/test_init.py | 8 +- tests/components/lovelace/test_init.py | 14 +- tests/components/media_player/test_init.py | 4 +- .../persistent_notification/test_init.py | 6 +- tests/components/test_websocket_api.py | 558 ------------------ tests/components/websocket_api/__init__.py | 2 + tests/components/websocket_api/conftest.py | 35 ++ tests/components/websocket_api/test_auth.py | 186 ++++++ .../components/websocket_api/test_commands.py | 260 ++++++++ tests/components/websocket_api/test_init.py | 92 +++ 21 files changed, 1041 insertions(+), 1003 deletions(-) rename homeassistant/components/{websocket_api.py => websocket_api/__init__.py} (52%) create mode 100644 homeassistant/components/websocket_api/commands.py create mode 100644 homeassistant/components/websocket_api/const.py create mode 100644 homeassistant/components/websocket_api/decorators.py create mode 100644 homeassistant/components/websocket_api/messages.py delete mode 100644 tests/components/test_websocket_api.py create mode 100644 tests/components/websocket_api/__init__.py create mode 100644 tests/components/websocket_api/conftest.py create mode 100644 tests/components/websocket_api/test_auth.py create mode 100644 tests/components/websocket_api/test_commands.py create mode 100644 tests/components/websocket_api/test_init.py diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index b32236e499d..95f0cddf320 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -452,27 +452,23 @@ class CameraMjpegStream(CameraView): raise web.HTTPBadRequest() -@callback -def websocket_camera_thumbnail(hass, connection, msg): +@websocket_api.async_response +async def websocket_camera_thumbnail(hass, connection, msg): """Handle get camera thumbnail websocket command. Async friendly. """ - async def send_camera_still(): - """Send a camera still.""" - try: - image = await async_get_image(hass, msg['entity_id']) - connection.send_message_outside(websocket_api.result_message( - msg['id'], { - 'content_type': image.content_type, - 'content': base64.b64encode(image.content).decode('utf-8') - } - )) - except HomeAssistantError: - connection.send_message_outside(websocket_api.error_message( - msg['id'], 'image_fetch_failed', 'Unable to fetch image')) - - hass.async_add_job(send_camera_still()) + try: + image = await async_get_image(hass, msg['entity_id']) + connection.send_message_outside(websocket_api.result_message( + msg['id'], { + 'content_type': image.content_type, + 'content': base64.b64encode(image.content).decode('utf-8') + } + )) + except HomeAssistantError: + connection.send_message_outside(websocket_api.error_message( + msg['id'], 'image_fetch_failed', 'Unable to fetch image')) async def async_handle_snapshot_service(camera, service): diff --git a/homeassistant/components/config/auth.py b/homeassistant/components/config/auth.py index 6f00b03dedb..17dd132d4b4 100644 --- a/homeassistant/components/config/auth.py +++ b/homeassistant/components/config/auth.py @@ -3,6 +3,7 @@ import voluptuous as vol from homeassistant.core import callback from homeassistant.components import websocket_api +from homeassistant.components.websocket_api.decorators import require_owner WS_TYPE_LIST = 'config/auth/list' @@ -41,7 +42,7 @@ async def async_setup(hass): @callback -@websocket_api.require_owner +@require_owner def websocket_list(hass, connection, msg): """Return a list of users.""" async def send_users(): @@ -55,7 +56,7 @@ def websocket_list(hass, connection, msg): @callback -@websocket_api.require_owner +@require_owner def websocket_delete(hass, connection, msg): """Delete a user.""" async def delete_user(): @@ -82,7 +83,7 @@ def websocket_delete(hass, connection, msg): @callback -@websocket_api.require_owner +@require_owner def websocket_create(hass, connection, msg): """Create a user.""" async def create_user(): diff --git a/homeassistant/components/config/auth_provider_homeassistant.py b/homeassistant/components/config/auth_provider_homeassistant.py index 960e8f5e7b4..8f0c969a808 100644 --- a/homeassistant/components/config/auth_provider_homeassistant.py +++ b/homeassistant/components/config/auth_provider_homeassistant.py @@ -4,6 +4,7 @@ import voluptuous as vol from homeassistant.auth.providers import homeassistant as auth_ha from homeassistant.core import callback from homeassistant.components import websocket_api +from homeassistant.components.websocket_api.decorators import require_owner WS_TYPE_CREATE = 'config/auth_provider/homeassistant/create' @@ -55,7 +56,7 @@ def _get_provider(hass): @callback -@websocket_api.require_owner +@require_owner def websocket_create(hass, connection, msg): """Create credentials and attach to a user.""" async def create_creds(): @@ -96,7 +97,7 @@ def websocket_create(hass, connection, msg): @callback -@websocket_api.require_owner +@require_owner def websocket_delete(hass, connection, msg): """Delete username and related credential.""" async def delete_creds(): diff --git a/homeassistant/components/config/entity_registry.py b/homeassistant/components/config/entity_registry.py index 0f9abf167e5..18d66ec623a 100644 --- a/homeassistant/components/config/entity_registry.py +++ b/homeassistant/components/config/entity_registry.py @@ -4,6 +4,8 @@ import voluptuous as vol from homeassistant.core import callback from homeassistant.helpers.entity_registry import async_get_registry from homeassistant.components import websocket_api +from homeassistant.components.websocket_api.const import ERR_NOT_FOUND +from homeassistant.components.websocket_api.decorators import async_response from homeassistant.helpers import config_validation as cv DEPENDENCIES = ['websocket_api'] @@ -46,89 +48,77 @@ async def async_setup(hass): return True -@callback -def websocket_list_entities(hass, connection, msg): +@async_response +async def websocket_list_entities(hass, connection, msg): """Handle list registry entries command. Async friendly. """ - async def retrieve_entities(): - """Get entities from registry.""" - registry = await async_get_registry(hass) - connection.send_message_outside(websocket_api.result_message( - msg['id'], [{ - 'config_entry_id': entry.config_entry_id, - 'device_id': entry.device_id, - 'disabled_by': entry.disabled_by, - 'entity_id': entry.entity_id, - 'name': entry.name, - 'platform': entry.platform, - } for entry in registry.entities.values()] - )) - - hass.async_add_job(retrieve_entities()) + registry = await async_get_registry(hass) + connection.send_message_outside(websocket_api.result_message( + msg['id'], [{ + 'config_entry_id': entry.config_entry_id, + 'device_id': entry.device_id, + 'disabled_by': entry.disabled_by, + 'entity_id': entry.entity_id, + 'name': entry.name, + 'platform': entry.platform, + } for entry in registry.entities.values()] + )) -@callback -def websocket_get_entity(hass, connection, msg): +@async_response +async def websocket_get_entity(hass, connection, msg): """Handle get entity registry entry command. Async friendly. """ - async def retrieve_entity(): - """Get entity from registry.""" - registry = await async_get_registry(hass) - entry = registry.entities.get(msg['entity_id']) + registry = await async_get_registry(hass) + entry = registry.entities.get(msg['entity_id']) - if entry is None: - connection.send_message_outside(websocket_api.error_message( - msg['id'], websocket_api.ERR_NOT_FOUND, 'Entity not found')) - return + if entry is None: + connection.send_message_outside(websocket_api.error_message( + msg['id'], ERR_NOT_FOUND, 'Entity not found')) + return - connection.send_message_outside(websocket_api.result_message( - msg['id'], _entry_dict(entry) - )) - - hass.async_add_job(retrieve_entity()) + connection.send_message_outside(websocket_api.result_message( + msg['id'], _entry_dict(entry) + )) -@callback -def websocket_update_entity(hass, connection, msg): +@async_response +async def websocket_update_entity(hass, connection, msg): """Handle get camera thumbnail websocket command. Async friendly. """ - async def update_entity(): - """Get entity from registry.""" - registry = await async_get_registry(hass) + registry = await async_get_registry(hass) - if msg['entity_id'] not in registry.entities: - connection.send_message_outside(websocket_api.error_message( - msg['id'], websocket_api.ERR_NOT_FOUND, 'Entity not found')) - return + if msg['entity_id'] not in registry.entities: + connection.send_message_outside(websocket_api.error_message( + msg['id'], ERR_NOT_FOUND, 'Entity not found')) + return - changes = {} + changes = {} - if 'name' in msg: - changes['name'] = msg['name'] + if 'name' in msg: + changes['name'] = msg['name'] - if 'new_entity_id' in msg: - changes['new_entity_id'] = msg['new_entity_id'] + if 'new_entity_id' in msg: + changes['new_entity_id'] = msg['new_entity_id'] - try: - if changes: - entry = registry.async_update_entity( - msg['entity_id'], **changes) - except ValueError as err: - connection.send_message_outside(websocket_api.error_message( - msg['id'], 'invalid_info', str(err) - )) - else: - connection.send_message_outside(websocket_api.result_message( - msg['id'], _entry_dict(entry) - )) - - hass.async_create_task(update_entity()) + try: + if changes: + entry = registry.async_update_entity( + msg['entity_id'], **changes) + except ValueError as err: + connection.send_message_outside(websocket_api.error_message( + msg['id'], 'invalid_info', str(err) + )) + else: + connection.send_message_outside(websocket_api.result_message( + msg['id'], _entry_dict(entry) + )) @callback diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 235ca8d5b2d..85016df7262 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -28,7 +28,6 @@ from homeassistant.const import ( SERVICE_TOGGLE, SERVICE_TURN_OFF, SERVICE_TURN_ON, SERVICE_VOLUME_DOWN, SERVICE_VOLUME_MUTE, SERVICE_VOLUME_SET, SERVICE_VOLUME_UP, STATE_IDLE, STATE_OFF, STATE_PLAYING, STATE_UNKNOWN) -from homeassistant.core import callback from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa @@ -865,8 +864,8 @@ class MediaPlayerImageView(HomeAssistantView): body=data, content_type=content_type, headers=headers) -@callback -def websocket_handle_thumbnail(hass, connection, msg): +@websocket_api.async_response +async def websocket_handle_thumbnail(hass, connection, msg): """Handle get media player cover command. Async friendly. @@ -879,20 +878,16 @@ def websocket_handle_thumbnail(hass, connection, msg): msg['id'], 'entity_not_found', 'Entity not found')) return - async def send_image(): - """Send image.""" - data, content_type = await player.async_get_media_image() + data, content_type = await player.async_get_media_image() - if data is None: - connection.send_message_outside(websocket_api.error_message( - msg['id'], 'thumbnail_fetch_failed', - 'Failed to fetch thumbnail')) - return + if data is None: + connection.send_message_outside(websocket_api.error_message( + msg['id'], 'thumbnail_fetch_failed', + 'Failed to fetch thumbnail')) + return - connection.send_message_outside(websocket_api.result_message( - msg['id'], { - 'content_type': content_type, - 'content': base64.b64encode(data).decode('utf-8') - })) - - hass.async_add_job(send_image()) + connection.send_message_outside(websocket_api.result_message( + msg['id'], { + 'content_type': content_type, + 'content': base64.b64encode(data).decode('utf-8') + })) diff --git a/homeassistant/components/websocket_api.py b/homeassistant/components/websocket_api/__init__.py similarity index 52% rename from homeassistant/components/websocket_api.py rename to homeassistant/components/websocket_api/__init__.py index 4e7c186facc..448256e31fd 100644 --- a/homeassistant/components/websocket_api.py +++ b/homeassistant/components/websocket_api/__init__.py @@ -7,7 +7,7 @@ https://developers.home-assistant.io/docs/external_api_websocket.html import asyncio from concurrent import futures from contextlib import suppress -from functools import partial, wraps +from functools import partial import json import logging @@ -15,20 +15,18 @@ from aiohttp import web import voluptuous as vol from voluptuous.humanize import humanize_error -from homeassistant.const import ( - MATCH_ALL, EVENT_TIME_CHANGED, EVENT_HOMEASSISTANT_STOP, - __version__) -from homeassistant.core import Context, callback, HomeAssistant +from homeassistant.const import EVENT_HOMEASSISTANT_STOP, __version__ +from homeassistant.core import Context, callback from homeassistant.loader import bind_hass from homeassistant.helpers.json import JSONEncoder -from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.service import async_get_all_descriptions from homeassistant.components.http import HomeAssistantView from homeassistant.components.http.auth import validate_password from homeassistant.components.http.const import KEY_AUTHENTICATED from homeassistant.components.http.ban import process_wrong_login, \ process_success_login +from . import commands, const, decorators, messages + DOMAIN = 'websocket_api' URL = '/api/websocket' @@ -36,87 +34,32 @@ DEPENDENCIES = ('http',) MAX_PENDING_MSG = 512 -ERR_ID_REUSE = 1 -ERR_INVALID_FORMAT = 2 -ERR_NOT_FOUND = 3 -ERR_UNKNOWN_COMMAND = 4 -ERR_UNKNOWN_ERROR = 5 - -TYPE_AUTH = 'auth' -TYPE_AUTH_INVALID = 'auth_invalid' -TYPE_AUTH_OK = 'auth_ok' -TYPE_AUTH_REQUIRED = 'auth_required' -TYPE_CALL_SERVICE = 'call_service' -TYPE_EVENT = 'event' -TYPE_GET_CONFIG = 'get_config' -TYPE_GET_SERVICES = 'get_services' -TYPE_GET_STATES = 'get_states' -TYPE_PING = 'ping' -TYPE_PONG = 'pong' -TYPE_RESULT = 'result' -TYPE_SUBSCRIBE_EVENTS = 'subscribe_events' -TYPE_UNSUBSCRIBE_EVENTS = 'unsubscribe_events' _LOGGER = logging.getLogger(__name__) JSON_DUMP = partial(json.dumps, cls=JSONEncoder) +TYPE_AUTH = 'auth' +TYPE_AUTH_INVALID = 'auth_invalid' +TYPE_AUTH_OK = 'auth_ok' +TYPE_AUTH_REQUIRED = 'auth_required' + + +# Backwards compat +# pylint: disable=invalid-name +BASE_COMMAND_MESSAGE_SCHEMA = messages.BASE_COMMAND_MESSAGE_SCHEMA +error_message = messages.error_message +result_message = messages.result_message +async_response = decorators.async_response +ws_require_user = decorators.ws_require_user +# pylint: enable=invalid-name + AUTH_MESSAGE_SCHEMA = vol.Schema({ vol.Required('type'): TYPE_AUTH, vol.Exclusive('api_password', 'auth'): str, vol.Exclusive('access_token', 'auth'): str, }) -# Minimal requirements of a message -MINIMAL_MESSAGE_SCHEMA = vol.Schema({ - vol.Required('id'): cv.positive_int, - vol.Required('type'): cv.string, -}, extra=vol.ALLOW_EXTRA) -# Base schema to extend by message handlers -BASE_COMMAND_MESSAGE_SCHEMA = vol.Schema({ - vol.Required('id'): cv.positive_int, -}) - - -SCHEMA_SUBSCRIBE_EVENTS = BASE_COMMAND_MESSAGE_SCHEMA.extend({ - vol.Required('type'): TYPE_SUBSCRIBE_EVENTS, - vol.Optional('event_type', default=MATCH_ALL): str, -}) - - -SCHEMA_UNSUBSCRIBE_EVENTS = BASE_COMMAND_MESSAGE_SCHEMA.extend({ - vol.Required('type'): TYPE_UNSUBSCRIBE_EVENTS, - vol.Required('subscription'): cv.positive_int, -}) - - -SCHEMA_CALL_SERVICE = BASE_COMMAND_MESSAGE_SCHEMA.extend({ - vol.Required('type'): TYPE_CALL_SERVICE, - vol.Required('domain'): str, - vol.Required('service'): str, - vol.Optional('service_data'): dict -}) - - -SCHEMA_GET_STATES = BASE_COMMAND_MESSAGE_SCHEMA.extend({ - vol.Required('type'): TYPE_GET_STATES, -}) - - -SCHEMA_GET_SERVICES = BASE_COMMAND_MESSAGE_SCHEMA.extend({ - vol.Required('type'): TYPE_GET_SERVICES, -}) - - -SCHEMA_GET_CONFIG = BASE_COMMAND_MESSAGE_SCHEMA.extend({ - vol.Required('type'): TYPE_GET_CONFIG, -}) - - -SCHEMA_PING = BASE_COMMAND_MESSAGE_SCHEMA.extend({ - vol.Required('type'): TYPE_PING, -}) - # Define the possible errors that occur when connections are cancelled. # Originally, this was just asyncio.CancelledError, but issue #9546 showed @@ -148,46 +91,6 @@ def auth_invalid_message(message): } -def event_message(iden, event): - """Return an event message.""" - return { - 'id': iden, - 'type': TYPE_EVENT, - 'event': event.as_dict(), - } - - -def error_message(iden, code, message): - """Return an error result message.""" - return { - 'id': iden, - 'type': TYPE_RESULT, - 'success': False, - 'error': { - 'code': code, - 'message': message, - }, - } - - -def pong_message(iden): - """Return a pong message.""" - return { - 'id': iden, - 'type': TYPE_PONG, - } - - -def result_message(iden, result=None): - """Return a success result message.""" - return { - 'id': iden, - 'type': TYPE_RESULT, - 'success': True, - 'result': result, - } - - @bind_hass @callback def async_register_command(hass, command, handler, schema): @@ -198,43 +101,10 @@ def async_register_command(hass, command, handler, schema): handlers[command] = (handler, schema) -def require_owner(func): - """Websocket decorator to require user to be an owner.""" - @wraps(func) - def with_owner(hass, connection, msg): - """Check owner and call function.""" - user = connection.request.get('hass_user') - - if user is None or not user.is_owner: - connection.to_write.put_nowait(error_message( - msg['id'], 'unauthorized', 'This command is for owners only.')) - return - - func(hass, connection, msg) - - return with_owner - - async def async_setup(hass, config): """Initialize the websocket API.""" hass.http.register_view(WebsocketAPIView) - - async_register_command(hass, TYPE_SUBSCRIBE_EVENTS, - handle_subscribe_events, SCHEMA_SUBSCRIBE_EVENTS) - async_register_command(hass, TYPE_UNSUBSCRIBE_EVENTS, - handle_unsubscribe_events, - SCHEMA_UNSUBSCRIBE_EVENTS) - async_register_command(hass, TYPE_CALL_SERVICE, - handle_call_service, SCHEMA_CALL_SERVICE) - async_register_command(hass, TYPE_GET_STATES, - handle_get_states, SCHEMA_GET_STATES) - async_register_command(hass, TYPE_GET_SERVICES, - handle_get_services, SCHEMA_GET_SERVICES) - async_register_command(hass, TYPE_GET_CONFIG, - handle_get_config, SCHEMA_GET_CONFIG) - async_register_command(hass, TYPE_PING, - handle_ping, SCHEMA_PING) - + commands.async_register_commands(hass) return True @@ -389,19 +259,19 @@ class ActiveConnection: while msg: self.debug("Received", msg) - msg = MINIMAL_MESSAGE_SCHEMA(msg) + msg = messages.MINIMAL_MESSAGE_SCHEMA(msg) cur_id = msg['id'] if cur_id <= last_id: - self.to_write.put_nowait(error_message( - cur_id, ERR_ID_REUSE, + self.to_write.put_nowait(messages.error_message( + cur_id, const.ERR_ID_REUSE, 'Identifier values have to increase.')) elif msg['type'] not in handlers: self.log_error( 'Received invalid command: {}'.format(msg['type'])) - self.to_write.put_nowait(error_message( - cur_id, ERR_UNKNOWN_COMMAND, + self.to_write.put_nowait(messages.error_message( + cur_id, const.ERR_UNKNOWN_COMMAND, 'Unknown command.')) else: @@ -410,8 +280,8 @@ class ActiveConnection: handler(self.hass, self, schema(msg)) except Exception: # pylint: disable=broad-except _LOGGER.exception('Error handling message: %s', msg) - self.to_write.put_nowait(error_message( - cur_id, ERR_UNKNOWN_ERROR, + self.to_write.put_nowait(messages.error_message( + cur_id, const.ERR_UNKNOWN_ERROR, 'Unknown error.')) last_id = cur_id @@ -435,8 +305,8 @@ class ActiveConnection: else: iden = None - final_message = error_message( - iden, ERR_INVALID_FORMAT, error_msg) + final_message = messages.error_message( + iden, const.ERR_INVALID_FORMAT, error_msg) except TypeError as err: if wsock.closed: @@ -485,170 +355,3 @@ class ActiveConnection: self.debug("Closed connection") return wsock - - -def async_response(func): - """Decorate an async function to handle WebSocket API messages.""" - async def handle_msg_response(hass, connection, msg): - """Create a response and handle exception.""" - try: - await func(hass, connection, msg) - except Exception: # pylint: disable=broad-except - _LOGGER.exception("Unexpected exception") - connection.send_message_outside(error_message( - msg['id'], 'unknown', 'Unexpected error occurred')) - - @callback - @wraps(func) - def schedule_handler(hass, connection, msg): - """Schedule the handler.""" - hass.async_create_task(handle_msg_response(hass, connection, msg)) - - return schedule_handler - - -@callback -def handle_subscribe_events(hass, connection, msg): - """Handle subscribe events command. - - Async friendly. - """ - async def forward_events(event): - """Forward events to websocket.""" - if event.event_type == EVENT_TIME_CHANGED: - return - - connection.send_message_outside(event_message(msg['id'], event)) - - connection.event_listeners[msg['id']] = hass.bus.async_listen( - msg['event_type'], forward_events) - - connection.to_write.put_nowait(result_message(msg['id'])) - - -@callback -def handle_unsubscribe_events(hass, connection, msg): - """Handle unsubscribe events command. - - Async friendly. - """ - subscription = msg['subscription'] - - if subscription in connection.event_listeners: - connection.event_listeners.pop(subscription)() - connection.to_write.put_nowait(result_message(msg['id'])) - else: - connection.to_write.put_nowait(error_message( - msg['id'], ERR_NOT_FOUND, 'Subscription not found.')) - - -@async_response -async def handle_call_service(hass, connection, msg): - """Handle call service command. - - Async friendly. - """ - blocking = True - if (msg['domain'] == 'homeassistant' and - msg['service'] in ['restart', 'stop']): - blocking = False - await hass.services.async_call( - msg['domain'], msg['service'], msg.get('service_data'), blocking, - connection.context(msg)) - connection.send_message_outside(result_message(msg['id'])) - - -@callback -def handle_get_states(hass, connection, msg): - """Handle get states command. - - Async friendly. - """ - connection.to_write.put_nowait(result_message( - msg['id'], hass.states.async_all())) - - -@async_response -async def handle_get_services(hass, connection, msg): - """Handle get services command. - - Async friendly. - """ - descriptions = await async_get_all_descriptions(hass) - connection.send_message_outside( - result_message(msg['id'], descriptions)) - - -@callback -def handle_get_config(hass, connection, msg): - """Handle get config command. - - Async friendly. - """ - connection.to_write.put_nowait(result_message( - msg['id'], hass.config.as_dict())) - - -@callback -def handle_ping(hass, connection, msg): - """Handle ping command. - - Async friendly. - """ - connection.to_write.put_nowait(pong_message(msg['id'])) - - -def ws_require_user( - only_owner=False, only_system_user=False, allow_system_user=True, - only_active_user=True, only_inactive_user=False): - """Decorate function validating login user exist in current WS connection. - - Will write out error message if not authenticated. - """ - def validator(func): - """Decorate func.""" - @wraps(func) - def check_current_user(hass: HomeAssistant, - connection: ActiveConnection, - msg): - """Check current user.""" - def output_error(message_id, message): - """Output error message.""" - connection.send_message_outside(error_message( - msg['id'], message_id, message)) - - if connection.user is None: - output_error('no_user', 'Not authenticated as a user') - return - - if only_owner and not connection.user.is_owner: - output_error('only_owner', 'Only allowed as owner') - return - - if (only_system_user and - not connection.user.system_generated): - output_error('only_system_user', - 'Only allowed as system user') - return - - if (not allow_system_user - and connection.user.system_generated): - output_error('not_system_user', 'Not allowed as system user') - return - - if (only_active_user and - not connection.user.is_active): - output_error('only_active_user', - 'Only allowed as active user') - return - - if only_inactive_user and connection.user.is_active: - output_error('only_inactive_user', - 'Not allowed as active user') - return - - return func(hass, connection, msg) - - return check_current_user - - return validator diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py new file mode 100644 index 00000000000..c9808f3a692 --- /dev/null +++ b/homeassistant/components/websocket_api/commands.py @@ -0,0 +1,183 @@ +"""Commands part of Websocket API.""" +import voluptuous as vol + +from homeassistant.const import MATCH_ALL, EVENT_TIME_CHANGED +from homeassistant.core import callback +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.service import async_get_all_descriptions + +from . import const, decorators, messages + + +TYPE_CALL_SERVICE = 'call_service' +TYPE_EVENT = 'event' +TYPE_GET_CONFIG = 'get_config' +TYPE_GET_SERVICES = 'get_services' +TYPE_GET_STATES = 'get_states' +TYPE_PING = 'ping' +TYPE_PONG = 'pong' +TYPE_SUBSCRIBE_EVENTS = 'subscribe_events' +TYPE_UNSUBSCRIBE_EVENTS = 'unsubscribe_events' + + +@callback +def async_register_commands(hass): + """Register commands.""" + async_reg = hass.components.websocket_api.async_register_command + async_reg(TYPE_SUBSCRIBE_EVENTS, handle_subscribe_events, + SCHEMA_SUBSCRIBE_EVENTS) + async_reg(TYPE_UNSUBSCRIBE_EVENTS, handle_unsubscribe_events, + SCHEMA_UNSUBSCRIBE_EVENTS) + async_reg(TYPE_CALL_SERVICE, handle_call_service, SCHEMA_CALL_SERVICE) + async_reg(TYPE_GET_STATES, handle_get_states, SCHEMA_GET_STATES) + async_reg(TYPE_GET_SERVICES, handle_get_services, SCHEMA_GET_SERVICES) + async_reg(TYPE_GET_CONFIG, handle_get_config, SCHEMA_GET_CONFIG) + async_reg(TYPE_PING, handle_ping, SCHEMA_PING) + + +SCHEMA_SUBSCRIBE_EVENTS = messages.BASE_COMMAND_MESSAGE_SCHEMA.extend({ + vol.Required('type'): TYPE_SUBSCRIBE_EVENTS, + vol.Optional('event_type', default=MATCH_ALL): str, +}) + + +SCHEMA_UNSUBSCRIBE_EVENTS = messages.BASE_COMMAND_MESSAGE_SCHEMA.extend({ + vol.Required('type'): TYPE_UNSUBSCRIBE_EVENTS, + vol.Required('subscription'): cv.positive_int, +}) + + +SCHEMA_CALL_SERVICE = messages.BASE_COMMAND_MESSAGE_SCHEMA.extend({ + vol.Required('type'): TYPE_CALL_SERVICE, + vol.Required('domain'): str, + vol.Required('service'): str, + vol.Optional('service_data'): dict +}) + + +SCHEMA_GET_STATES = messages.BASE_COMMAND_MESSAGE_SCHEMA.extend({ + vol.Required('type'): TYPE_GET_STATES, +}) + + +SCHEMA_GET_SERVICES = messages.BASE_COMMAND_MESSAGE_SCHEMA.extend({ + vol.Required('type'): TYPE_GET_SERVICES, +}) + + +SCHEMA_GET_CONFIG = messages.BASE_COMMAND_MESSAGE_SCHEMA.extend({ + vol.Required('type'): TYPE_GET_CONFIG, +}) + + +SCHEMA_PING = messages.BASE_COMMAND_MESSAGE_SCHEMA.extend({ + vol.Required('type'): TYPE_PING, +}) + + +def event_message(iden, event): + """Return an event message.""" + return { + 'id': iden, + 'type': TYPE_EVENT, + 'event': event.as_dict(), + } + + +def pong_message(iden): + """Return a pong message.""" + return { + 'id': iden, + 'type': TYPE_PONG, + } + + +@callback +def handle_subscribe_events(hass, connection, msg): + """Handle subscribe events command. + + Async friendly. + """ + async def forward_events(event): + """Forward events to websocket.""" + if event.event_type == EVENT_TIME_CHANGED: + return + + connection.send_message_outside(event_message(msg['id'], event)) + + connection.event_listeners[msg['id']] = hass.bus.async_listen( + msg['event_type'], forward_events) + + connection.to_write.put_nowait(messages.result_message(msg['id'])) + + +@callback +def handle_unsubscribe_events(hass, connection, msg): + """Handle unsubscribe events command. + + Async friendly. + """ + subscription = msg['subscription'] + + if subscription in connection.event_listeners: + connection.event_listeners.pop(subscription)() + connection.to_write.put_nowait(messages.result_message(msg['id'])) + else: + connection.to_write.put_nowait(messages.error_message( + msg['id'], const.ERR_NOT_FOUND, 'Subscription not found.')) + + +@decorators.async_response +async def handle_call_service(hass, connection, msg): + """Handle call service command. + + Async friendly. + """ + blocking = True + if (msg['domain'] == 'homeassistant' and + msg['service'] in ['restart', 'stop']): + blocking = False + await hass.services.async_call( + msg['domain'], msg['service'], msg.get('service_data'), blocking, + connection.context(msg)) + connection.send_message_outside(messages.result_message(msg['id'])) + + +@callback +def handle_get_states(hass, connection, msg): + """Handle get states command. + + Async friendly. + """ + connection.to_write.put_nowait(messages.result_message( + msg['id'], hass.states.async_all())) + + +@decorators.async_response +async def handle_get_services(hass, connection, msg): + """Handle get services command. + + Async friendly. + """ + descriptions = await async_get_all_descriptions(hass) + connection.send_message_outside( + messages.result_message(msg['id'], descriptions)) + + +@callback +def handle_get_config(hass, connection, msg): + """Handle get config command. + + Async friendly. + """ + connection.to_write.put_nowait(messages.result_message( + msg['id'], hass.config.as_dict())) + + +@callback +def handle_ping(hass, connection, msg): + """Handle ping command. + + Async friendly. + """ + connection.to_write.put_nowait(pong_message(msg['id'])) diff --git a/homeassistant/components/websocket_api/const.py b/homeassistant/components/websocket_api/const.py new file mode 100644 index 00000000000..cbc56b168c6 --- /dev/null +++ b/homeassistant/components/websocket_api/const.py @@ -0,0 +1,8 @@ +"""Websocket constants.""" +ERR_ID_REUSE = 1 +ERR_INVALID_FORMAT = 2 +ERR_NOT_FOUND = 3 +ERR_UNKNOWN_COMMAND = 4 +ERR_UNKNOWN_ERROR = 5 + +TYPE_RESULT = 'result' diff --git a/homeassistant/components/websocket_api/decorators.py b/homeassistant/components/websocket_api/decorators.py new file mode 100644 index 00000000000..df32dd06d2b --- /dev/null +++ b/homeassistant/components/websocket_api/decorators.py @@ -0,0 +1,101 @@ +"""Decorators for the Websocket API.""" +from functools import wraps +import logging + +from homeassistant.core import callback + +from . import messages + + +_LOGGER = logging.getLogger(__name__) + + +def async_response(func): + """Decorate an async function to handle WebSocket API messages.""" + async def handle_msg_response(hass, connection, msg): + """Create a response and handle exception.""" + try: + await func(hass, connection, msg) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + connection.send_message_outside(messages.error_message( + msg['id'], 'unknown', 'Unexpected error occurred')) + + @callback + @wraps(func) + def schedule_handler(hass, connection, msg): + """Schedule the handler.""" + hass.async_create_task(handle_msg_response(hass, connection, msg)) + + return schedule_handler + + +def require_owner(func): + """Websocket decorator to require user to be an owner.""" + @wraps(func) + def with_owner(hass, connection, msg): + """Check owner and call function.""" + user = connection.request.get('hass_user') + + if user is None or not user.is_owner: + connection.to_write.put_nowait(messages.error_message( + msg['id'], 'unauthorized', 'This command is for owners only.')) + return + + func(hass, connection, msg) + + return with_owner + + +def ws_require_user( + only_owner=False, only_system_user=False, allow_system_user=True, + only_active_user=True, only_inactive_user=False): + """Decorate function validating login user exist in current WS connection. + + Will write out error message if not authenticated. + """ + def validator(func): + """Decorate func.""" + @wraps(func) + def check_current_user(hass, connection, msg): + """Check current user.""" + def output_error(message_id, message): + """Output error message.""" + connection.send_message_outside(messages.error_message( + msg['id'], message_id, message)) + + if connection.user is None: + output_error('no_user', 'Not authenticated as a user') + return + + if only_owner and not connection.user.is_owner: + output_error('only_owner', 'Only allowed as owner') + return + + if (only_system_user and + not connection.user.system_generated): + output_error('only_system_user', + 'Only allowed as system user') + return + + if (not allow_system_user + and connection.user.system_generated): + output_error('not_system_user', 'Not allowed as system user') + return + + if (only_active_user and + not connection.user.is_active): + output_error('only_active_user', + 'Only allowed as active user') + return + + if only_inactive_user and connection.user.is_active: + output_error('only_inactive_user', + 'Not allowed as active user') + return + + return func(hass, connection, msg) + + return check_current_user + + return validator diff --git a/homeassistant/components/websocket_api/messages.py b/homeassistant/components/websocket_api/messages.py new file mode 100644 index 00000000000..d616b6ad670 --- /dev/null +++ b/homeassistant/components/websocket_api/messages.py @@ -0,0 +1,42 @@ +"""Message templates for websocket commands.""" + +import voluptuous as vol + +from homeassistant.helpers import config_validation as cv + +from . import const + + +# Minimal requirements of a message +MINIMAL_MESSAGE_SCHEMA = vol.Schema({ + vol.Required('id'): cv.positive_int, + vol.Required('type'): cv.string, +}, extra=vol.ALLOW_EXTRA) + +# Base schema to extend by message handlers +BASE_COMMAND_MESSAGE_SCHEMA = vol.Schema({ + vol.Required('id'): cv.positive_int, +}) + + +def result_message(iden, result=None): + """Return a success result message.""" + return { + 'id': iden, + 'type': const.TYPE_RESULT, + 'success': True, + 'result': result, + } + + +def error_message(iden, code, message): + """Return an error result message.""" + return { + 'id': iden, + 'type': const.TYPE_RESULT, + 'success': False, + 'error': { + 'code': code, + 'message': message, + }, + } diff --git a/tests/components/camera/test_init.py b/tests/components/camera/test_init.py index 2129e39a43c..6b98f378ef0 100644 --- a/tests/components/camera/test_init.py +++ b/tests/components/camera/test_init.py @@ -7,7 +7,8 @@ import pytest from homeassistant.setup import setup_component, async_setup_component from homeassistant.const import ATTR_ENTITY_PICTURE -from homeassistant.components import camera, http, websocket_api +from homeassistant.components import camera, http +from homeassistant.components.websocket_api.const import TYPE_RESULT from homeassistant.exceptions import HomeAssistantError from homeassistant.util.async_ import run_coroutine_threadsafe @@ -150,7 +151,7 @@ async def test_webocket_camera_thumbnail(hass, hass_ws_client, mock_camera): msg = await client.receive_json() assert msg['id'] == 5 - assert msg['type'] == websocket_api.TYPE_RESULT + assert msg['type'] == TYPE_RESULT assert msg['success'] assert msg['result']['content_type'] == 'image/jpeg' assert msg['result']['content'] == \ diff --git a/tests/components/frontend/test_init.py b/tests/components/frontend/test_init.py index b29c8cfb12f..2e78e0441a3 100644 --- a/tests/components/frontend/test_init.py +++ b/tests/components/frontend/test_init.py @@ -9,7 +9,7 @@ from homeassistant.setup import async_setup_component from homeassistant.components.frontend import ( DOMAIN, CONF_JS_VERSION, CONF_THEMES, CONF_EXTRA_HTML_URL, CONF_EXTRA_HTML_URL_ES5) -from homeassistant.components import websocket_api as wapi +from homeassistant.components.websocket_api.const import TYPE_RESULT from tests.common import mock_coro @@ -213,7 +213,7 @@ async def test_missing_themes(hass, hass_ws_client): msg = await client.receive_json() assert msg['id'] == 5 - assert msg['type'] == wapi.TYPE_RESULT + assert msg['type'] == TYPE_RESULT assert msg['success'] assert msg['result']['default_theme'] == 'default' assert msg['result']['themes'] == {} @@ -252,7 +252,7 @@ async def test_get_panels(hass, hass_ws_client): msg = await client.receive_json() assert msg['id'] == 5 - assert msg['type'] == wapi.TYPE_RESULT + assert msg['type'] == TYPE_RESULT assert msg['success'] assert msg['result']['map']['component_name'] == 'map' assert msg['result']['map']['url_path'] == 'map' @@ -275,7 +275,7 @@ async def test_get_translations(hass, hass_ws_client): msg = await client.receive_json() assert msg['id'] == 5 - assert msg['type'] == wapi.TYPE_RESULT + assert msg['type'] == TYPE_RESULT assert msg['success'] assert msg['result'] == {'resources': {'lang': 'nl'}} diff --git a/tests/components/lovelace/test_init.py b/tests/components/lovelace/test_init.py index 3bb7c0675ea..0fde6de902c 100644 --- a/tests/components/lovelace/test_init.py +++ b/tests/components/lovelace/test_init.py @@ -3,7 +3,7 @@ from unittest.mock import patch from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import async_setup_component -from homeassistant.components import websocket_api as wapi +from homeassistant.components.websocket_api.const import TYPE_RESULT async def test_deprecated_lovelace_ui(hass, hass_ws_client): @@ -20,7 +20,7 @@ async def test_deprecated_lovelace_ui(hass, hass_ws_client): msg = await client.receive_json() assert msg['id'] == 5 - assert msg['type'] == wapi.TYPE_RESULT + assert msg['type'] == TYPE_RESULT assert msg['success'] assert msg['result'] == {'hello': 'world'} @@ -39,7 +39,7 @@ async def test_deprecated_lovelace_ui_not_found(hass, hass_ws_client): msg = await client.receive_json() assert msg['id'] == 5 - assert msg['type'] == wapi.TYPE_RESULT + assert msg['type'] == TYPE_RESULT assert msg['success'] is False assert msg['error']['code'] == 'file_not_found' @@ -58,7 +58,7 @@ async def test_deprecated_lovelace_ui_load_err(hass, hass_ws_client): msg = await client.receive_json() assert msg['id'] == 5 - assert msg['type'] == wapi.TYPE_RESULT + assert msg['type'] == TYPE_RESULT assert msg['success'] is False assert msg['error']['code'] == 'load_error' @@ -77,7 +77,7 @@ async def test_lovelace_ui(hass, hass_ws_client): msg = await client.receive_json() assert msg['id'] == 5 - assert msg['type'] == wapi.TYPE_RESULT + assert msg['type'] == TYPE_RESULT assert msg['success'] assert msg['result'] == {'hello': 'world'} @@ -96,7 +96,7 @@ async def test_lovelace_ui_not_found(hass, hass_ws_client): msg = await client.receive_json() assert msg['id'] == 5 - assert msg['type'] == wapi.TYPE_RESULT + assert msg['type'] == TYPE_RESULT assert msg['success'] is False assert msg['error']['code'] == 'file_not_found' @@ -115,6 +115,6 @@ async def test_lovelace_ui_load_err(hass, hass_ws_client): msg = await client.receive_json() assert msg['id'] == 5 - assert msg['type'] == wapi.TYPE_RESULT + assert msg['type'] == TYPE_RESULT assert msg['success'] is False assert msg['error']['code'] == 'load_error' diff --git a/tests/components/media_player/test_init.py b/tests/components/media_player/test_init.py index 5d632d4de0b..808c6e4f50f 100644 --- a/tests/components/media_player/test_init.py +++ b/tests/components/media_player/test_init.py @@ -3,7 +3,7 @@ import base64 from unittest.mock import patch from homeassistant.setup import async_setup_component -from homeassistant.components import websocket_api +from homeassistant.components.websocket_api.const import TYPE_RESULT from tests.common import mock_coro @@ -30,7 +30,7 @@ async def test_get_panels(hass, hass_ws_client): msg = await client.receive_json() assert msg['id'] == 5 - assert msg['type'] == websocket_api.TYPE_RESULT + assert msg['type'] == TYPE_RESULT assert msg['success'] assert msg['result']['content_type'] == 'image/jpeg' assert msg['result']['content'] == \ diff --git a/tests/components/persistent_notification/test_init.py b/tests/components/persistent_notification/test_init.py index 6acc796a108..5df106a5327 100644 --- a/tests/components/persistent_notification/test_init.py +++ b/tests/components/persistent_notification/test_init.py @@ -1,5 +1,5 @@ """The tests for the persistent notification component.""" -from homeassistant.components import websocket_api +from homeassistant.components.websocket_api.const import TYPE_RESULT from homeassistant.setup import setup_component, async_setup_component import homeassistant.components.persistent_notification as pn @@ -151,7 +151,7 @@ async def test_ws_get_notifications(hass, hass_ws_client): }) msg = await client.receive_json() assert msg['id'] == 5 - assert msg['type'] == websocket_api.TYPE_RESULT + assert msg['type'] == TYPE_RESULT assert msg['success'] notifications = msg['result'] assert len(notifications) == 0 @@ -165,7 +165,7 @@ async def test_ws_get_notifications(hass, hass_ws_client): }) msg = await client.receive_json() assert msg['id'] == 6 - assert msg['type'] == websocket_api.TYPE_RESULT + assert msg['type'] == TYPE_RESULT assert msg['success'] notifications = msg['result'] assert len(notifications) == 1 diff --git a/tests/components/test_websocket_api.py b/tests/components/test_websocket_api.py deleted file mode 100644 index cf74081adb1..00000000000 --- a/tests/components/test_websocket_api.py +++ /dev/null @@ -1,558 +0,0 @@ -"""Tests for the Home Assistant Websocket API.""" -import asyncio -from unittest.mock import patch, Mock - -from aiohttp import WSMsgType -from async_timeout import timeout -import pytest - -from homeassistant.core import callback -from homeassistant.components import websocket_api as wapi -from homeassistant.setup import async_setup_component - -from tests.common import mock_coro, async_mock_service - -API_PASSWORD = 'test1234' - - -@pytest.fixture -def websocket_client(hass, hass_ws_client): - """Create a websocket client.""" - return hass.loop.run_until_complete(hass_ws_client(hass)) - - -@pytest.fixture -def no_auth_websocket_client(hass, loop, aiohttp_client): - """Websocket connection that requires authentication.""" - assert loop.run_until_complete( - async_setup_component(hass, 'websocket_api', { - 'http': { - 'api_password': API_PASSWORD - } - })) - - client = loop.run_until_complete(aiohttp_client(hass.http.app)) - ws = loop.run_until_complete(client.ws_connect(wapi.URL)) - - auth_ok = loop.run_until_complete(ws.receive_json()) - assert auth_ok['type'] == wapi.TYPE_AUTH_REQUIRED - - yield ws - - if not ws.closed: - loop.run_until_complete(ws.close()) - - -@pytest.fixture -def mock_low_queue(): - """Mock a low queue.""" - with patch.object(wapi, 'MAX_PENDING_MSG', 5): - yield - - -@asyncio.coroutine -def test_auth_via_msg(no_auth_websocket_client): - """Test authenticating.""" - yield from no_auth_websocket_client.send_json({ - 'type': wapi.TYPE_AUTH, - 'api_password': API_PASSWORD - }) - - msg = yield from no_auth_websocket_client.receive_json() - - assert msg['type'] == wapi.TYPE_AUTH_OK - - -@asyncio.coroutine -def test_auth_via_msg_incorrect_pass(no_auth_websocket_client): - """Test authenticating.""" - with patch('homeassistant.components.websocket_api.process_wrong_login', - return_value=mock_coro()) as mock_process_wrong_login: - yield from no_auth_websocket_client.send_json({ - 'type': wapi.TYPE_AUTH, - 'api_password': API_PASSWORD + 'wrong' - }) - - msg = yield from no_auth_websocket_client.receive_json() - - assert mock_process_wrong_login.called - assert msg['type'] == wapi.TYPE_AUTH_INVALID - assert msg['message'] == 'Invalid access token or password' - - -@asyncio.coroutine -def test_pre_auth_only_auth_allowed(no_auth_websocket_client): - """Verify that before authentication, only auth messages are allowed.""" - yield from no_auth_websocket_client.send_json({ - 'type': wapi.TYPE_CALL_SERVICE, - 'domain': 'domain_test', - 'service': 'test_service', - 'service_data': { - 'hello': 'world' - } - }) - - msg = yield from no_auth_websocket_client.receive_json() - - assert msg['type'] == wapi.TYPE_AUTH_INVALID - assert msg['message'].startswith('Message incorrectly formatted') - - -@asyncio.coroutine -def test_invalid_message_format(websocket_client): - """Test sending invalid JSON.""" - yield from websocket_client.send_json({'type': 5}) - - msg = yield from websocket_client.receive_json() - - assert msg['type'] == wapi.TYPE_RESULT - error = msg['error'] - assert error['code'] == wapi.ERR_INVALID_FORMAT - assert error['message'].startswith('Message incorrectly formatted') - - -@asyncio.coroutine -def test_invalid_json(websocket_client): - """Test sending invalid JSON.""" - yield from websocket_client.send_str('this is not JSON') - - msg = yield from websocket_client.receive() - - assert msg.type == WSMsgType.close - - -@asyncio.coroutine -def test_quiting_hass(hass, websocket_client): - """Test sending invalid JSON.""" - with patch.object(hass.loop, 'stop'): - yield from hass.async_stop() - - msg = yield from websocket_client.receive() - - assert msg.type == WSMsgType.CLOSE - - -@asyncio.coroutine -def test_call_service(hass, websocket_client): - """Test call service command.""" - calls = [] - - @callback - def service_call(call): - calls.append(call) - - hass.services.async_register('domain_test', 'test_service', service_call) - - yield from websocket_client.send_json({ - 'id': 5, - 'type': wapi.TYPE_CALL_SERVICE, - 'domain': 'domain_test', - 'service': 'test_service', - 'service_data': { - 'hello': 'world' - } - }) - - msg = yield from websocket_client.receive_json() - assert msg['id'] == 5 - assert msg['type'] == wapi.TYPE_RESULT - assert msg['success'] - - assert len(calls) == 1 - call = calls[0] - - assert call.domain == 'domain_test' - assert call.service == 'test_service' - assert call.data == {'hello': 'world'} - - -@asyncio.coroutine -def test_subscribe_unsubscribe_events(hass, websocket_client): - """Test subscribe/unsubscribe events command.""" - init_count = sum(hass.bus.async_listeners().values()) - - yield from websocket_client.send_json({ - 'id': 5, - 'type': wapi.TYPE_SUBSCRIBE_EVENTS, - 'event_type': 'test_event' - }) - - msg = yield from websocket_client.receive_json() - assert msg['id'] == 5 - assert msg['type'] == wapi.TYPE_RESULT - assert msg['success'] - - # Verify we have a new listener - assert sum(hass.bus.async_listeners().values()) == init_count + 1 - - hass.bus.async_fire('ignore_event') - hass.bus.async_fire('test_event', {'hello': 'world'}) - hass.bus.async_fire('ignore_event') - - with timeout(3, loop=hass.loop): - msg = yield from websocket_client.receive_json() - - assert msg['id'] == 5 - assert msg['type'] == wapi.TYPE_EVENT - event = msg['event'] - - assert event['event_type'] == 'test_event' - assert event['data'] == {'hello': 'world'} - assert event['origin'] == 'LOCAL' - - yield from websocket_client.send_json({ - 'id': 6, - 'type': wapi.TYPE_UNSUBSCRIBE_EVENTS, - 'subscription': 5 - }) - - msg = yield from websocket_client.receive_json() - assert msg['id'] == 6 - assert msg['type'] == wapi.TYPE_RESULT - assert msg['success'] - - # Check our listener got unsubscribed - assert sum(hass.bus.async_listeners().values()) == init_count - - -@asyncio.coroutine -def test_get_states(hass, websocket_client): - """Test get_states command.""" - hass.states.async_set('greeting.hello', 'world') - hass.states.async_set('greeting.bye', 'universe') - - yield from websocket_client.send_json({ - 'id': 5, - 'type': wapi.TYPE_GET_STATES, - }) - - msg = yield from websocket_client.receive_json() - assert msg['id'] == 5 - assert msg['type'] == wapi.TYPE_RESULT - assert msg['success'] - - states = [] - for state in hass.states.async_all(): - state = state.as_dict() - state['last_changed'] = state['last_changed'].isoformat() - state['last_updated'] = state['last_updated'].isoformat() - states.append(state) - - assert msg['result'] == states - - -@asyncio.coroutine -def test_get_services(hass, websocket_client): - """Test get_services command.""" - yield from websocket_client.send_json({ - 'id': 5, - 'type': wapi.TYPE_GET_SERVICES, - }) - - msg = yield from websocket_client.receive_json() - assert msg['id'] == 5 - assert msg['type'] == wapi.TYPE_RESULT - assert msg['success'] - assert msg['result'] == hass.services.async_services() - - -@asyncio.coroutine -def test_get_config(hass, websocket_client): - """Test get_config command.""" - yield from websocket_client.send_json({ - 'id': 5, - 'type': wapi.TYPE_GET_CONFIG, - }) - - msg = yield from websocket_client.receive_json() - assert msg['id'] == 5 - assert msg['type'] == wapi.TYPE_RESULT - assert msg['success'] - - if 'components' in msg['result']: - msg['result']['components'] = set(msg['result']['components']) - if 'whitelist_external_dirs' in msg['result']: - msg['result']['whitelist_external_dirs'] = \ - set(msg['result']['whitelist_external_dirs']) - - assert msg['result'] == hass.config.as_dict() - - -@asyncio.coroutine -def test_ping(websocket_client): - """Test get_panels command.""" - yield from websocket_client.send_json({ - 'id': 5, - 'type': wapi.TYPE_PING, - }) - - msg = yield from websocket_client.receive_json() - assert msg['id'] == 5 - assert msg['type'] == wapi.TYPE_PONG - - -@asyncio.coroutine -def test_pending_msg_overflow(hass, mock_low_queue, websocket_client): - """Test get_panels command.""" - for idx in range(10): - yield from websocket_client.send_json({ - 'id': idx + 1, - 'type': wapi.TYPE_PING, - }) - msg = yield from websocket_client.receive() - assert msg.type == WSMsgType.close - - -@asyncio.coroutine -def test_unknown_command(websocket_client): - """Test get_panels command.""" - yield from websocket_client.send_json({ - 'id': 5, - 'type': 'unknown_command', - }) - - msg = yield from websocket_client.receive_json() - assert not msg['success'] - assert msg['error']['code'] == wapi.ERR_UNKNOWN_COMMAND - - -async def test_auth_active_with_token(hass, aiohttp_client, hass_access_token): - """Test authenticating with a token.""" - assert await async_setup_component(hass, 'websocket_api', { - 'http': { - 'api_password': API_PASSWORD - } - }) - - client = await aiohttp_client(hass.http.app) - - async with client.ws_connect(wapi.URL) as ws: - with patch('homeassistant.auth.AuthManager.active') as auth_active: - auth_active.return_value = True - auth_msg = await ws.receive_json() - assert auth_msg['type'] == wapi.TYPE_AUTH_REQUIRED - - await ws.send_json({ - 'type': wapi.TYPE_AUTH, - 'access_token': hass_access_token - }) - - auth_msg = await ws.receive_json() - assert auth_msg['type'] == wapi.TYPE_AUTH_OK - - -async def test_auth_active_user_inactive(hass, aiohttp_client, - hass_access_token): - """Test authenticating with a token.""" - refresh_token = await hass.auth.async_validate_access_token( - hass_access_token) - refresh_token.user.is_active = False - assert await async_setup_component(hass, 'websocket_api', { - 'http': { - 'api_password': API_PASSWORD - } - }) - - client = await aiohttp_client(hass.http.app) - - async with client.ws_connect(wapi.URL) as ws: - with patch('homeassistant.auth.AuthManager.active') as auth_active: - auth_active.return_value = True - auth_msg = await ws.receive_json() - assert auth_msg['type'] == wapi.TYPE_AUTH_REQUIRED - - await ws.send_json({ - 'type': wapi.TYPE_AUTH, - 'access_token': hass_access_token - }) - - auth_msg = await ws.receive_json() - assert auth_msg['type'] == wapi.TYPE_AUTH_INVALID - - -async def test_auth_active_with_password_not_allow(hass, aiohttp_client): - """Test authenticating with a token.""" - assert await async_setup_component(hass, 'websocket_api', { - 'http': { - 'api_password': API_PASSWORD - } - }) - - client = await aiohttp_client(hass.http.app) - - async with client.ws_connect(wapi.URL) as ws: - with patch('homeassistant.auth.AuthManager.active', - return_value=True): - auth_msg = await ws.receive_json() - assert auth_msg['type'] == wapi.TYPE_AUTH_REQUIRED - - await ws.send_json({ - 'type': wapi.TYPE_AUTH, - 'api_password': API_PASSWORD - }) - - auth_msg = await ws.receive_json() - assert auth_msg['type'] == wapi.TYPE_AUTH_INVALID - - -async def test_auth_legacy_support_with_password(hass, aiohttp_client): - """Test authenticating with a token.""" - assert await async_setup_component(hass, 'websocket_api', { - 'http': { - 'api_password': API_PASSWORD - } - }) - - client = await aiohttp_client(hass.http.app) - - async with client.ws_connect(wapi.URL) as ws: - with patch('homeassistant.auth.AuthManager.active', - return_value=True),\ - patch('homeassistant.auth.AuthManager.support_legacy', - return_value=True): - auth_msg = await ws.receive_json() - assert auth_msg['type'] == wapi.TYPE_AUTH_REQUIRED - - await ws.send_json({ - 'type': wapi.TYPE_AUTH, - 'api_password': API_PASSWORD - }) - - auth_msg = await ws.receive_json() - assert auth_msg['type'] == wapi.TYPE_AUTH_OK - - -async def test_auth_with_invalid_token(hass, aiohttp_client): - """Test authenticating with a token.""" - assert await async_setup_component(hass, 'websocket_api', { - 'http': { - 'api_password': API_PASSWORD - } - }) - - client = await aiohttp_client(hass.http.app) - - async with client.ws_connect(wapi.URL) as ws: - with patch('homeassistant.auth.AuthManager.active') as auth_active: - auth_active.return_value = True - auth_msg = await ws.receive_json() - assert auth_msg['type'] == wapi.TYPE_AUTH_REQUIRED - - await ws.send_json({ - 'type': wapi.TYPE_AUTH, - 'access_token': 'incorrect' - }) - - auth_msg = await ws.receive_json() - assert auth_msg['type'] == wapi.TYPE_AUTH_INVALID - - -async def test_call_service_context_with_user(hass, aiohttp_client, - hass_access_token): - """Test that the user is set in the service call context.""" - assert await async_setup_component(hass, 'websocket_api', { - 'http': { - 'api_password': API_PASSWORD - } - }) - - calls = async_mock_service(hass, 'domain_test', 'test_service') - client = await aiohttp_client(hass.http.app) - - async with client.ws_connect(wapi.URL) as ws: - with patch('homeassistant.auth.AuthManager.active') as auth_active: - auth_active.return_value = True - auth_msg = await ws.receive_json() - assert auth_msg['type'] == wapi.TYPE_AUTH_REQUIRED - - await ws.send_json({ - 'type': wapi.TYPE_AUTH, - 'access_token': hass_access_token - }) - - auth_msg = await ws.receive_json() - assert auth_msg['type'] == wapi.TYPE_AUTH_OK - - await ws.send_json({ - 'id': 5, - 'type': wapi.TYPE_CALL_SERVICE, - 'domain': 'domain_test', - 'service': 'test_service', - 'service_data': { - 'hello': 'world' - } - }) - - msg = await ws.receive_json() - assert msg['success'] - - refresh_token = await hass.auth.async_validate_access_token( - hass_access_token) - - assert len(calls) == 1 - call = calls[0] - assert call.domain == 'domain_test' - assert call.service == 'test_service' - assert call.data == {'hello': 'world'} - assert call.context.user_id == refresh_token.user.id - - -async def test_call_service_context_no_user(hass, aiohttp_client): - """Test that connection without user sets context.""" - assert await async_setup_component(hass, 'websocket_api', { - 'http': { - 'api_password': API_PASSWORD - } - }) - - calls = async_mock_service(hass, 'domain_test', 'test_service') - client = await aiohttp_client(hass.http.app) - - async with client.ws_connect(wapi.URL) as ws: - auth_msg = await ws.receive_json() - assert auth_msg['type'] == wapi.TYPE_AUTH_REQUIRED - - await ws.send_json({ - 'type': wapi.TYPE_AUTH, - 'api_password': API_PASSWORD - }) - - auth_msg = await ws.receive_json() - assert auth_msg['type'] == wapi.TYPE_AUTH_OK - - await ws.send_json({ - 'id': 5, - 'type': wapi.TYPE_CALL_SERVICE, - 'domain': 'domain_test', - 'service': 'test_service', - 'service_data': { - 'hello': 'world' - } - }) - - msg = await ws.receive_json() - assert msg['success'] - - assert len(calls) == 1 - call = calls[0] - assert call.domain == 'domain_test' - assert call.service == 'test_service' - assert call.data == {'hello': 'world'} - assert call.context.user_id is None - - -async def test_handler_failing(hass, websocket_client): - """Test a command that raises.""" - hass.components.websocket_api.async_register_command( - 'bla', Mock(side_effect=TypeError), - wapi.BASE_COMMAND_MESSAGE_SCHEMA.extend({'type': 'bla'})) - await websocket_client.send_json({ - 'id': 5, - 'type': 'bla', - }) - - msg = await websocket_client.receive_json() - assert msg['id'] == 5 - assert msg['type'] == wapi.TYPE_RESULT - assert not msg['success'] - assert msg['error']['code'] == wapi.ERR_UNKNOWN_ERROR diff --git a/tests/components/websocket_api/__init__.py b/tests/components/websocket_api/__init__.py new file mode 100644 index 00000000000..c218c6165d4 --- /dev/null +++ b/tests/components/websocket_api/__init__.py @@ -0,0 +1,2 @@ +"""Tests for the websocket API.""" +API_PASSWORD = 'test1234' diff --git a/tests/components/websocket_api/conftest.py b/tests/components/websocket_api/conftest.py new file mode 100644 index 00000000000..063e0b43d1b --- /dev/null +++ b/tests/components/websocket_api/conftest.py @@ -0,0 +1,35 @@ +"""Fixtures for websocket tests.""" +import pytest + +from homeassistant.setup import async_setup_component +from homeassistant.components import websocket_api as wapi + +from . import API_PASSWORD + + +@pytest.fixture +def websocket_client(hass, hass_ws_client): + """Create a websocket client.""" + return hass.loop.run_until_complete(hass_ws_client(hass)) + + +@pytest.fixture +def no_auth_websocket_client(hass, loop, aiohttp_client): + """Websocket connection that requires authentication.""" + assert loop.run_until_complete( + async_setup_component(hass, 'websocket_api', { + 'http': { + 'api_password': API_PASSWORD + } + })) + + client = loop.run_until_complete(aiohttp_client(hass.http.app)) + ws = loop.run_until_complete(client.ws_connect(wapi.URL)) + + auth_ok = loop.run_until_complete(ws.receive_json()) + assert auth_ok['type'] == wapi.TYPE_AUTH_REQUIRED + + yield ws + + if not ws.closed: + loop.run_until_complete(ws.close()) diff --git a/tests/components/websocket_api/test_auth.py b/tests/components/websocket_api/test_auth.py new file mode 100644 index 00000000000..ee1de906fa1 --- /dev/null +++ b/tests/components/websocket_api/test_auth.py @@ -0,0 +1,186 @@ +"""Test auth of websocket API.""" +from unittest.mock import patch + +from homeassistant.components import websocket_api as wapi +from homeassistant.components.websocket_api import commands +from homeassistant.setup import async_setup_component + +from tests.common import mock_coro + +from . import API_PASSWORD + + +async def test_auth_via_msg(no_auth_websocket_client): + """Test authenticating.""" + await no_auth_websocket_client.send_json({ + 'type': wapi.TYPE_AUTH, + 'api_password': API_PASSWORD + }) + + msg = await no_auth_websocket_client.receive_json() + + assert msg['type'] == wapi.TYPE_AUTH_OK + + +async def test_auth_via_msg_incorrect_pass(no_auth_websocket_client): + """Test authenticating.""" + with patch('homeassistant.components.websocket_api.process_wrong_login', + return_value=mock_coro()) as mock_process_wrong_login: + await no_auth_websocket_client.send_json({ + 'type': wapi.TYPE_AUTH, + 'api_password': API_PASSWORD + 'wrong' + }) + + msg = await no_auth_websocket_client.receive_json() + + assert mock_process_wrong_login.called + assert msg['type'] == wapi.TYPE_AUTH_INVALID + assert msg['message'] == 'Invalid access token or password' + + +async def test_pre_auth_only_auth_allowed(no_auth_websocket_client): + """Verify that before authentication, only auth messages are allowed.""" + await no_auth_websocket_client.send_json({ + 'type': commands.TYPE_CALL_SERVICE, + 'domain': 'domain_test', + 'service': 'test_service', + 'service_data': { + 'hello': 'world' + } + }) + + msg = await no_auth_websocket_client.receive_json() + + assert msg['type'] == wapi.TYPE_AUTH_INVALID + assert msg['message'].startswith('Message incorrectly formatted') + + +async def test_auth_active_with_token(hass, aiohttp_client, hass_access_token): + """Test authenticating with a token.""" + assert await async_setup_component(hass, 'websocket_api', { + 'http': { + 'api_password': API_PASSWORD + } + }) + + client = await aiohttp_client(hass.http.app) + + async with client.ws_connect(wapi.URL) as ws: + with patch('homeassistant.auth.AuthManager.active') as auth_active: + auth_active.return_value = True + auth_msg = await ws.receive_json() + assert auth_msg['type'] == wapi.TYPE_AUTH_REQUIRED + + await ws.send_json({ + 'type': wapi.TYPE_AUTH, + 'access_token': hass_access_token + }) + + auth_msg = await ws.receive_json() + assert auth_msg['type'] == wapi.TYPE_AUTH_OK + + +async def test_auth_active_user_inactive(hass, aiohttp_client, + hass_access_token): + """Test authenticating with a token.""" + refresh_token = await hass.auth.async_validate_access_token( + hass_access_token) + refresh_token.user.is_active = False + assert await async_setup_component(hass, 'websocket_api', { + 'http': { + 'api_password': API_PASSWORD + } + }) + + client = await aiohttp_client(hass.http.app) + + async with client.ws_connect(wapi.URL) as ws: + with patch('homeassistant.auth.AuthManager.active') as auth_active: + auth_active.return_value = True + auth_msg = await ws.receive_json() + assert auth_msg['type'] == wapi.TYPE_AUTH_REQUIRED + + await ws.send_json({ + 'type': wapi.TYPE_AUTH, + 'access_token': hass_access_token + }) + + auth_msg = await ws.receive_json() + assert auth_msg['type'] == wapi.TYPE_AUTH_INVALID + + +async def test_auth_active_with_password_not_allow(hass, aiohttp_client): + """Test authenticating with a token.""" + assert await async_setup_component(hass, 'websocket_api', { + 'http': { + 'api_password': API_PASSWORD + } + }) + + client = await aiohttp_client(hass.http.app) + + async with client.ws_connect(wapi.URL) as ws: + with patch('homeassistant.auth.AuthManager.active', + return_value=True): + auth_msg = await ws.receive_json() + assert auth_msg['type'] == wapi.TYPE_AUTH_REQUIRED + + await ws.send_json({ + 'type': wapi.TYPE_AUTH, + 'api_password': API_PASSWORD + }) + + auth_msg = await ws.receive_json() + assert auth_msg['type'] == wapi.TYPE_AUTH_INVALID + + +async def test_auth_legacy_support_with_password(hass, aiohttp_client): + """Test authenticating with a token.""" + assert await async_setup_component(hass, 'websocket_api', { + 'http': { + 'api_password': API_PASSWORD + } + }) + + client = await aiohttp_client(hass.http.app) + + async with client.ws_connect(wapi.URL) as ws: + with patch('homeassistant.auth.AuthManager.active', + return_value=True),\ + patch('homeassistant.auth.AuthManager.support_legacy', + return_value=True): + auth_msg = await ws.receive_json() + assert auth_msg['type'] == wapi.TYPE_AUTH_REQUIRED + + await ws.send_json({ + 'type': wapi.TYPE_AUTH, + 'api_password': API_PASSWORD + }) + + auth_msg = await ws.receive_json() + assert auth_msg['type'] == wapi.TYPE_AUTH_OK + + +async def test_auth_with_invalid_token(hass, aiohttp_client): + """Test authenticating with a token.""" + assert await async_setup_component(hass, 'websocket_api', { + 'http': { + 'api_password': API_PASSWORD + } + }) + + client = await aiohttp_client(hass.http.app) + + async with client.ws_connect(wapi.URL) as ws: + with patch('homeassistant.auth.AuthManager.active') as auth_active: + auth_active.return_value = True + auth_msg = await ws.receive_json() + assert auth_msg['type'] == wapi.TYPE_AUTH_REQUIRED + + await ws.send_json({ + 'type': wapi.TYPE_AUTH, + 'access_token': 'incorrect' + }) + + auth_msg = await ws.receive_json() + assert auth_msg['type'] == wapi.TYPE_AUTH_INVALID diff --git a/tests/components/websocket_api/test_commands.py b/tests/components/websocket_api/test_commands.py new file mode 100644 index 00000000000..0eaf215afaa --- /dev/null +++ b/tests/components/websocket_api/test_commands.py @@ -0,0 +1,260 @@ +"""Tests for WebSocket API commands.""" +from unittest.mock import patch + +from async_timeout import timeout + +from homeassistant.core import callback +from homeassistant.components import websocket_api as wapi +from homeassistant.components.websocket_api import const, commands +from homeassistant.setup import async_setup_component + +from tests.common import async_mock_service + +from . import API_PASSWORD + + +async def test_call_service(hass, websocket_client): + """Test call service command.""" + calls = [] + + @callback + def service_call(call): + calls.append(call) + + hass.services.async_register('domain_test', 'test_service', service_call) + + await websocket_client.send_json({ + 'id': 5, + 'type': commands.TYPE_CALL_SERVICE, + 'domain': 'domain_test', + 'service': 'test_service', + 'service_data': { + 'hello': 'world' + } + }) + + msg = await websocket_client.receive_json() + assert msg['id'] == 5 + assert msg['type'] == const.TYPE_RESULT + assert msg['success'] + + assert len(calls) == 1 + call = calls[0] + + assert call.domain == 'domain_test' + assert call.service == 'test_service' + assert call.data == {'hello': 'world'} + + +async def test_subscribe_unsubscribe_events(hass, websocket_client): + """Test subscribe/unsubscribe events command.""" + init_count = sum(hass.bus.async_listeners().values()) + + await websocket_client.send_json({ + 'id': 5, + 'type': commands.TYPE_SUBSCRIBE_EVENTS, + 'event_type': 'test_event' + }) + + msg = await websocket_client.receive_json() + assert msg['id'] == 5 + assert msg['type'] == const.TYPE_RESULT + assert msg['success'] + + # Verify we have a new listener + assert sum(hass.bus.async_listeners().values()) == init_count + 1 + + hass.bus.async_fire('ignore_event') + hass.bus.async_fire('test_event', {'hello': 'world'}) + hass.bus.async_fire('ignore_event') + + with timeout(3, loop=hass.loop): + msg = await websocket_client.receive_json() + + assert msg['id'] == 5 + assert msg['type'] == commands.TYPE_EVENT + event = msg['event'] + + assert event['event_type'] == 'test_event' + assert event['data'] == {'hello': 'world'} + assert event['origin'] == 'LOCAL' + + await websocket_client.send_json({ + 'id': 6, + 'type': commands.TYPE_UNSUBSCRIBE_EVENTS, + 'subscription': 5 + }) + + msg = await websocket_client.receive_json() + assert msg['id'] == 6 + assert msg['type'] == const.TYPE_RESULT + assert msg['success'] + + # Check our listener got unsubscribed + assert sum(hass.bus.async_listeners().values()) == init_count + + +async def test_get_states(hass, websocket_client): + """Test get_states command.""" + hass.states.async_set('greeting.hello', 'world') + hass.states.async_set('greeting.bye', 'universe') + + await websocket_client.send_json({ + 'id': 5, + 'type': commands.TYPE_GET_STATES, + }) + + msg = await websocket_client.receive_json() + assert msg['id'] == 5 + assert msg['type'] == const.TYPE_RESULT + assert msg['success'] + + states = [] + for state in hass.states.async_all(): + state = state.as_dict() + state['last_changed'] = state['last_changed'].isoformat() + state['last_updated'] = state['last_updated'].isoformat() + states.append(state) + + assert msg['result'] == states + + +async def test_get_services(hass, websocket_client): + """Test get_services command.""" + await websocket_client.send_json({ + 'id': 5, + 'type': commands.TYPE_GET_SERVICES, + }) + + msg = await websocket_client.receive_json() + assert msg['id'] == 5 + assert msg['type'] == const.TYPE_RESULT + assert msg['success'] + assert msg['result'] == hass.services.async_services() + + +async def test_get_config(hass, websocket_client): + """Test get_config command.""" + await websocket_client.send_json({ + 'id': 5, + 'type': commands.TYPE_GET_CONFIG, + }) + + msg = await websocket_client.receive_json() + assert msg['id'] == 5 + assert msg['type'] == const.TYPE_RESULT + assert msg['success'] + + if 'components' in msg['result']: + msg['result']['components'] = set(msg['result']['components']) + if 'whitelist_external_dirs' in msg['result']: + msg['result']['whitelist_external_dirs'] = \ + set(msg['result']['whitelist_external_dirs']) + + assert msg['result'] == hass.config.as_dict() + + +async def test_ping(websocket_client): + """Test get_panels command.""" + await websocket_client.send_json({ + 'id': 5, + 'type': commands.TYPE_PING, + }) + + msg = await websocket_client.receive_json() + assert msg['id'] == 5 + assert msg['type'] == commands.TYPE_PONG + + +async def test_call_service_context_with_user(hass, aiohttp_client, + hass_access_token): + """Test that the user is set in the service call context.""" + assert await async_setup_component(hass, 'websocket_api', { + 'http': { + 'api_password': API_PASSWORD + } + }) + + calls = async_mock_service(hass, 'domain_test', 'test_service') + client = await aiohttp_client(hass.http.app) + + async with client.ws_connect(wapi.URL) as ws: + with patch('homeassistant.auth.AuthManager.active') as auth_active: + auth_active.return_value = True + auth_msg = await ws.receive_json() + assert auth_msg['type'] == wapi.TYPE_AUTH_REQUIRED + + await ws.send_json({ + 'type': wapi.TYPE_AUTH, + 'access_token': hass_access_token + }) + + auth_msg = await ws.receive_json() + assert auth_msg['type'] == wapi.TYPE_AUTH_OK + + await ws.send_json({ + 'id': 5, + 'type': commands.TYPE_CALL_SERVICE, + 'domain': 'domain_test', + 'service': 'test_service', + 'service_data': { + 'hello': 'world' + } + }) + + msg = await ws.receive_json() + assert msg['success'] + + refresh_token = await hass.auth.async_validate_access_token( + hass_access_token) + + assert len(calls) == 1 + call = calls[0] + assert call.domain == 'domain_test' + assert call.service == 'test_service' + assert call.data == {'hello': 'world'} + assert call.context.user_id == refresh_token.user.id + + +async def test_call_service_context_no_user(hass, aiohttp_client): + """Test that connection without user sets context.""" + assert await async_setup_component(hass, 'websocket_api', { + 'http': { + 'api_password': API_PASSWORD + } + }) + + calls = async_mock_service(hass, 'domain_test', 'test_service') + client = await aiohttp_client(hass.http.app) + + async with client.ws_connect(wapi.URL) as ws: + auth_msg = await ws.receive_json() + assert auth_msg['type'] == wapi.TYPE_AUTH_REQUIRED + + await ws.send_json({ + 'type': wapi.TYPE_AUTH, + 'api_password': API_PASSWORD + }) + + auth_msg = await ws.receive_json() + assert auth_msg['type'] == wapi.TYPE_AUTH_OK + + await ws.send_json({ + 'id': 5, + 'type': commands.TYPE_CALL_SERVICE, + 'domain': 'domain_test', + 'service': 'test_service', + 'service_data': { + 'hello': 'world' + } + }) + + msg = await ws.receive_json() + assert msg['success'] + + assert len(calls) == 1 + call = calls[0] + assert call.domain == 'domain_test' + assert call.service == 'test_service' + assert call.data == {'hello': 'world'} + assert call.context.user_id is None diff --git a/tests/components/websocket_api/test_init.py b/tests/components/websocket_api/test_init.py new file mode 100644 index 00000000000..97acc1210fc --- /dev/null +++ b/tests/components/websocket_api/test_init.py @@ -0,0 +1,92 @@ +"""Tests for the Home Assistant Websocket API.""" +import asyncio +from unittest.mock import patch, Mock + +from aiohttp import WSMsgType +import pytest + +from homeassistant.components import websocket_api as wapi +from homeassistant.components.websocket_api import const, commands, messages + + +@pytest.fixture +def mock_low_queue(): + """Mock a low queue.""" + with patch.object(wapi, 'MAX_PENDING_MSG', 5): + yield + + +@asyncio.coroutine +def test_invalid_message_format(websocket_client): + """Test sending invalid JSON.""" + yield from websocket_client.send_json({'type': 5}) + + msg = yield from websocket_client.receive_json() + + assert msg['type'] == const.TYPE_RESULT + error = msg['error'] + assert error['code'] == const.ERR_INVALID_FORMAT + assert error['message'].startswith('Message incorrectly formatted') + + +@asyncio.coroutine +def test_invalid_json(websocket_client): + """Test sending invalid JSON.""" + yield from websocket_client.send_str('this is not JSON') + + msg = yield from websocket_client.receive() + + assert msg.type == WSMsgType.close + + +@asyncio.coroutine +def test_quiting_hass(hass, websocket_client): + """Test sending invalid JSON.""" + with patch.object(hass.loop, 'stop'): + yield from hass.async_stop() + + msg = yield from websocket_client.receive() + + assert msg.type == WSMsgType.CLOSE + + +@asyncio.coroutine +def test_pending_msg_overflow(hass, mock_low_queue, websocket_client): + """Test get_panels command.""" + for idx in range(10): + yield from websocket_client.send_json({ + 'id': idx + 1, + 'type': commands.TYPE_PING, + }) + msg = yield from websocket_client.receive() + assert msg.type == WSMsgType.close + + +@asyncio.coroutine +def test_unknown_command(websocket_client): + """Test get_panels command.""" + yield from websocket_client.send_json({ + 'id': 5, + 'type': 'unknown_command', + }) + + msg = yield from websocket_client.receive_json() + assert not msg['success'] + assert msg['error']['code'] == const.ERR_UNKNOWN_COMMAND + + +async def test_handler_failing(hass, websocket_client): + """Test a command that raises.""" + hass.components.websocket_api.async_register_command( + 'bla', Mock(side_effect=TypeError), + messages.BASE_COMMAND_MESSAGE_SCHEMA.extend({'type': 'bla'})) + await websocket_client.send_json({ + 'id': 5, + 'type': 'bla', + }) + + msg = await websocket_client.receive_json() + assert msg['id'] == 5 + assert msg['type'] == const.TYPE_RESULT + assert not msg['success'] + assert msg['error']['code'] == const.ERR_UNKNOWN_ERROR From 3137099348581f9fa477070c47a8058632e6a58d Mon Sep 17 00:00:00 2001 From: Timmo Date: Mon, 1 Oct 2018 11:51:39 +0100 Subject: [PATCH 130/247] :hammer: update errors (#17029) --- homeassistant/components/switch/rest.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/switch/rest.py b/homeassistant/components/switch/rest.py index 9dac5130593..78eceeb421d 100644 --- a/homeassistant/components/switch/rest.py +++ b/homeassistant/components/switch/rest.py @@ -129,7 +129,7 @@ class RestSwitch(SwitchDevice): "Can't turn on %s. Is resource/endpoint offline?", self._resource) except (asyncio.TimeoutError, aiohttp.ClientError): - _LOGGER.error("Error while turn on %s", self._resource) + _LOGGER.error("Error while switching on %s", self._resource) async def async_turn_off(self, **kwargs): """Turn the device off.""" @@ -144,7 +144,7 @@ class RestSwitch(SwitchDevice): "Can't turn off %s. Is resource/endpoint offline?", self._resource) except (asyncio.TimeoutError, aiohttp.ClientError): - _LOGGER.error("Error while turn off %s", self._resource) + _LOGGER.error("Error while switching off %s", self._resource) async def set_device_state(self, body): """Send a state update to the device.""" @@ -160,8 +160,10 @@ class RestSwitch(SwitchDevice): """Get the current state, catching errors.""" try: await self.get_device_state(self.hass) - except (asyncio.TimeoutError, aiohttp.ClientError): - _LOGGER.exception("Error while fetch data.") + except asyncio.TimeoutError: + _LOGGER.exception("Timed out while fetching data") + except aiohttp.ClientError as err: + _LOGGER.exception("Error while fetching data: %s", err) async def get_device_state(self, hass): """Get the latest data from REST API and update the state.""" From 68bda1c7321bec5fbf209a22100305efde6be6da Mon Sep 17 00:00:00 2001 From: Heiko Thiery Date: Mon, 1 Oct 2018 12:53:25 +0200 Subject: [PATCH 131/247] Add new device attributes to fritzbox climate (#17027) * Add new device attributes to fritzbox climate With Fitz!OS 7 new parameters are introduced. Signed-off-by: Heiko Thiery * update requirements Signed-off-by: Heiko Thiery --- homeassistant/components/climate/fritzbox.py | 19 ++++++++++++++++--- homeassistant/components/fritzbox.py | 9 ++++++--- requirements_all.txt | 2 +- 3 files changed, 23 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/climate/fritzbox.py b/homeassistant/components/climate/fritzbox.py index 3eedb89a3b7..f2d13ee92f6 100644 --- a/homeassistant/components/climate/fritzbox.py +++ b/homeassistant/components/climate/fritzbox.py @@ -10,13 +10,15 @@ import requests from homeassistant.components.fritzbox import DOMAIN as FRITZBOX_DOMAIN from homeassistant.components.fritzbox import ( - ATTR_STATE_DEVICE_LOCKED, ATTR_STATE_BATTERY_LOW, ATTR_STATE_LOCKED) + ATTR_STATE_DEVICE_LOCKED, ATTR_STATE_BATTERY_LOW, ATTR_STATE_HOLIDAY_MODE, + ATTR_STATE_LOCKED, ATTR_STATE_SUMMER_MODE, + ATTR_STATE_WINDOW_OPEN) from homeassistant.components.climate import ( ATTR_OPERATION_MODE, ClimateDevice, STATE_ECO, STATE_HEAT, STATE_MANUAL, STATE_OFF, STATE_ON, SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE) from homeassistant.const import ( - ATTR_TEMPERATURE, PRECISION_HALVES, TEMP_CELSIUS) + ATTR_BATTERY_LEVEL, ATTR_TEMPERATURE, PRECISION_HALVES, TEMP_CELSIUS) DEPENDENCIES = ['fritzbox'] _LOGGER = logging.getLogger(__name__) @@ -151,10 +153,21 @@ class FritzboxThermostat(ClimateDevice): def device_state_attributes(self): """Return the device specific state attributes.""" attrs = { + ATTR_STATE_BATTERY_LOW: self._device.battery_low, ATTR_STATE_DEVICE_LOCKED: self._device.device_lock, ATTR_STATE_LOCKED: self._device.lock, - ATTR_STATE_BATTERY_LOW: self._device.battery_low, } + + # the following attributes are available since fritzos 7 + if self._device.battery_level is not None: + attrs[ATTR_BATTERY_LEVEL] = self._device.battery_level + if self._device.holiday_active is not None: + attrs[ATTR_STATE_HOLIDAY_MODE] = self._device.holiday_active + if self._device.summer_active is not None: + attrs[ATTR_STATE_SUMMER_MODE] = self._device.summer_active + if ATTR_STATE_WINDOW_OPEN is not None: + attrs[ATTR_STATE_WINDOW_OPEN] = self._device.window_open + return attrs def update(self): diff --git a/homeassistant/components/fritzbox.py b/homeassistant/components/fritzbox.py index a3c35aaa597..49bc4c8f6e6 100644 --- a/homeassistant/components/fritzbox.py +++ b/homeassistant/components/fritzbox.py @@ -16,15 +16,18 @@ from homeassistant.helpers import discovery _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['pyfritzhome==0.3.7'] +REQUIREMENTS = ['pyfritzhome==0.4.0'] SUPPORTED_DOMAINS = ['climate', 'switch'] DOMAIN = 'fritzbox' -ATTR_STATE_DEVICE_LOCKED = 'device_locked' -ATTR_STATE_LOCKED = 'locked' ATTR_STATE_BATTERY_LOW = 'battery_low' +ATTR_STATE_DEVICE_LOCKED = 'device_locked' +ATTR_STATE_HOLIDAY_MODE = 'holiday_mode' +ATTR_STATE_LOCKED = 'locked' +ATTR_STATE_SUMMER_MODE = 'summer_mode' +ATTR_STATE_WINDOW_OPEN = 'window_open' CONFIG_SCHEMA = vol.Schema({ diff --git a/requirements_all.txt b/requirements_all.txt index 853e404c8b1..87c8ffdbb0a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -884,7 +884,7 @@ pyflic-homeassistant==0.4.dev0 pyfnip==0.2 # homeassistant.components.fritzbox -pyfritzhome==0.3.7 +pyfritzhome==0.4.0 # homeassistant.components.ifttt pyfttt==0.3 From 5bc2e78ab4ecc117e1416eb0851fbaf4de164b28 Mon Sep 17 00:00:00 2001 From: David De Sloovere Date: Mon, 1 Oct 2018 14:10:32 +0200 Subject: [PATCH 132/247] Bump Enphase_Envoy dependency for older models (#17032) --- homeassistant/components/sensor/enphase_envoy.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/enphase_envoy.py b/homeassistant/components/sensor/enphase_envoy.py index 7f8cff0f885..4bbf7eec01b 100644 --- a/homeassistant/components/sensor/enphase_envoy.py +++ b/homeassistant/components/sensor/enphase_envoy.py @@ -14,7 +14,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.const import (CONF_IP_ADDRESS, CONF_MONITORED_CONDITIONS) -REQUIREMENTS = ['envoy_reader==0.2'] +REQUIREMENTS = ['envoy_reader==0.3'] _LOGGER = logging.getLogger(__name__) SENSORS = { diff --git a/requirements_all.txt b/requirements_all.txt index 87c8ffdbb0a..2fac42738de 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -324,7 +324,7 @@ enocean==0.40 # envirophat==0.0.6 # homeassistant.components.sensor.enphase_envoy -envoy_reader==0.2 +envoy_reader==0.3 # homeassistant.components.sensor.season ephem==3.7.6.0 From c69a790edee37cd4027a191fab39565ca53fb4bc Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 1 Oct 2018 14:11:21 +0200 Subject: [PATCH 133/247] Add Hass.io discovery to MQTT (#16962) * Add Hass.io discovery to MQTT * Update en.json * Update __init__.py * Update config_flow.py * Update strings.json * Update test_config_flow.py * Lint * Ensure we don't send bad data in config entry --- .../components/mqtt/.translations/en.json | 9 ++- homeassistant/components/mqtt/__init__.py | 2 +- homeassistant/components/mqtt/config_flow.py | 63 +++++++++++++++++- homeassistant/components/mqtt/strings.json | 7 ++ tests/components/mqtt/test_config_flow.py | 66 ++++++++++++++++++- 5 files changed, 141 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/mqtt/.translations/en.json b/homeassistant/components/mqtt/.translations/en.json index c0b83a1323f..97c0c1c7091 100644 --- a/homeassistant/components/mqtt/.translations/en.json +++ b/homeassistant/components/mqtt/.translations/en.json @@ -17,8 +17,15 @@ }, "description": "Please enter the connection information of your MQTT broker.", "title": "MQTT" + }, + "hassio_confirm": { + "data": { + "discovery": "Enable discovery" + }, + "description": "Do you want to configure Home Assistant to connect to the MQTT broker provided by the hass.io add-on {addon}?", + "title": "MQTT Broker via Hass.io add-on" } }, "title": "MQTT" } -} \ No newline at end of file +} diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 62bbb8dc9c5..a9226117b5a 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -411,7 +411,7 @@ async def async_setup_entry(hass, entry): # If user didn't have configuration.yaml config, generate defaults if conf is None: conf = CONFIG_SCHEMA({ - DOMAIN: entry.data + DOMAIN: entry.data, })[DOMAIN] elif any(key in conf for key in entry.data): _LOGGER.warning( diff --git a/homeassistant/components/mqtt/config_flow.py b/homeassistant/components/mqtt/config_flow.py index 22072857b03..e0d1e692c60 100644 --- a/homeassistant/components/mqtt/config_flow.py +++ b/homeassistant/components/mqtt/config_flow.py @@ -5,7 +5,8 @@ import queue import voluptuous as vol from homeassistant import config_entries -from homeassistant.const import CONF_PASSWORD, CONF_PORT, CONF_USERNAME +from homeassistant.const import ( + CONF_PASSWORD, CONF_PORT, CONF_USERNAME, CONF_PROTOCOL) from .const import CONF_BROKER, CONF_DISCOVERY, DEFAULT_DISCOVERY @@ -17,6 +18,8 @@ class FlowHandler(config_entries.ConfigFlow): VERSION = 1 CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH + _hassio_discovery = None + async def async_step_user(self, user_input=None): """Handle a flow initialized by the user.""" if self._async_current_entries(): @@ -60,11 +63,65 @@ class FlowHandler(config_entries.ConfigFlow): return self.async_create_entry(title='configuration.yaml', data={}) + async def async_step_hassio(self, user_input=None): + """Receive a Hass.io discovery.""" + if self._async_current_entries(): + return self.async_abort(reason='single_instance_allowed') -def try_connection(broker, port, username, password): + self._hassio_discovery = user_input + + return await self.async_step_hassio_confirm() + + async def async_step_hassio_confirm(self, user_input=None): + """Confirm a Hass.io discovery.""" + errors = {} + + if user_input is not None: + data = self._hassio_discovery + can_connect = await self.hass.async_add_executor_job( + try_connection, + data[CONF_BROKER], + data[CONF_PORT], + data.get(CONF_USERNAME), + data.get(CONF_PASSWORD), + data.get(CONF_PROTOCOL) + ) + + if can_connect: + return self.async_create_entry( + title=data['addon'], data={ + CONF_BROKER: data[CONF_BROKER], + CONF_PORT: data[CONF_PORT], + CONF_USERNAME: data.get(CONF_USERNAME), + CONF_PASSWORD: data.get(CONF_PASSWORD), + CONF_PROTOCOL: data.get(CONF_PROTOCOL), + CONF_DISCOVERY: user_input[CONF_DISCOVERY], + }) + + errors['base'] = 'cannot_connect' + + return self.async_show_form( + step_id='hassio_confirm', + description_placeholders={ + 'addon': self._hassio_discovery['addon'] + }, + data_schema=vol.Schema({ + vol.Optional(CONF_DISCOVERY, default=DEFAULT_DISCOVERY): bool + }), + errors=errors, + ) + + +def try_connection(broker, port, username, password, protocol='3.1'): """Test if we can connect to an MQTT broker.""" import paho.mqtt.client as mqtt - client = mqtt.Client() + + if protocol == '3.1': + proto = mqtt.MQTTv31 + else: + proto = mqtt.MQTTv311 + + client = mqtt.Client(protocol=proto) if username and password: client.username_pw_set(username, password) diff --git a/homeassistant/components/mqtt/strings.json b/homeassistant/components/mqtt/strings.json index 0a2cb255cc4..40a68195f26 100644 --- a/homeassistant/components/mqtt/strings.json +++ b/homeassistant/components/mqtt/strings.json @@ -12,6 +12,13 @@ "password": "Password", "discovery": "Enable discovery" } + }, + "hassio_confirm": { + "title": "MQTT Broker via Hass.io add-on", + "description": "Do you want to configure Home Assistant to connect to the MQTT broker provided by the hass.io add-on {addon}?", + "data": { + "discovery": "Enable discovery" + } } }, "abort": { diff --git a/tests/components/mqtt/test_config_flow.py b/tests/components/mqtt/test_config_flow.py index 9f6be60c68b..08bb4e54a39 100644 --- a/tests/components/mqtt/test_config_flow.py +++ b/tests/components/mqtt/test_config_flow.py @@ -5,7 +5,7 @@ import pytest from homeassistant.setup import async_setup_component -from tests.common import mock_coro +from tests.common import mock_coro, MockConfigEntry @pytest.fixture(autouse=True) @@ -88,3 +88,67 @@ async def test_manual_config_set(hass, mock_try_connection, result = await hass.config_entries.flow.async_init( 'mqtt', context={'source': 'user'}) assert result['type'] == 'abort' + + +async def test_user_single_instance(hass): + """Test we only allow a single config flow.""" + MockConfigEntry(domain='mqtt').add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + 'mqtt', context={'source': 'user'}) + assert result['type'] == 'abort' + assert result['reason'] == 'single_instance_allowed' + + +async def test_hassio_single_instance(hass): + """Test we only allow a single config flow.""" + MockConfigEntry(domain='mqtt').add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + 'mqtt', context={'source': 'hassio'}) + assert result['type'] == 'abort' + assert result['reason'] == 'single_instance_allowed' + + +async def test_hassio_confirm(hass, mock_try_connection, + mock_finish_setup): + """Test we can finish a config flow.""" + mock_try_connection.return_value = True + + result = await hass.config_entries.flow.async_init( + 'mqtt', + data={ + 'addon': 'Mock Addon', + 'broker': 'mock-broker', + 'port': 1883, + 'username': 'mock-user', + 'password': 'mock-pass', + 'protocol': '3.1.1' + }, + context={'source': 'hassio'} + ) + assert result['type'] == 'form' + assert result['step_id'] == 'hassio_confirm' + assert result['description_placeholders'] == { + 'addon': 'Mock Addon', + } + + result = await hass.config_entries.flow.async_configure( + result['flow_id'], { + 'discovery': True, + } + ) + + assert result['type'] == 'create_entry' + assert result['result'].data == { + 'broker': 'mock-broker', + 'port': 1883, + 'username': 'mock-user', + 'password': 'mock-pass', + 'protocol': '3.1.1', + 'discovery': True, + } + # Check we tried the connection + assert len(mock_try_connection.mock_calls) == 1 + # Check config entry got setup + assert len(mock_finish_setup.mock_calls) == 1 From e5c0e4336d5d3d763ddfd3e9ddde31385013b3b7 Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Mon, 1 Oct 2018 14:41:35 +0200 Subject: [PATCH 134/247] Update coverage to exclude not tested file (#17039) --- .coveragerc | 1 + 1 file changed, 1 insertion(+) diff --git a/.coveragerc b/.coveragerc index 5e522016096..e13e9fe0002 100644 --- a/.coveragerc +++ b/.coveragerc @@ -714,6 +714,7 @@ omit = homeassistant/components/sensor/mqtt_room.py homeassistant/components/sensor/mvglive.py homeassistant/components/sensor/nederlandse_spoorwegen.py + homeassistant/components/sensor/netatmo_public.py homeassistant/components/sensor/netdata.py homeassistant/components/sensor/netdata_public.py homeassistant/components/sensor/neurio_energy.py From f0fbdd6a26f5eb70f2645a104f2521a4213e627c Mon Sep 17 00:00:00 2001 From: Josh Anderson Date: Mon, 1 Oct 2018 13:43:54 +0100 Subject: [PATCH 135/247] Send headers with REST switch GET request (#17036) --- homeassistant/components/switch/rest.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/switch/rest.py b/homeassistant/components/switch/rest.py index 78eceeb421d..9b8f889a8ae 100644 --- a/homeassistant/components/switch/rest.py +++ b/homeassistant/components/switch/rest.py @@ -170,7 +170,8 @@ class RestSwitch(SwitchDevice): websession = async_get_clientsession(hass) with async_timeout.timeout(self._timeout, loop=hass.loop): - req = await websession.get(self._resource, auth=self._auth) + req = await websession.get(self._resource, auth=self._auth, + headers=self._headers) text = await req.text() if self._is_on_template is not None: From b5e3d8c337ed435792edaa2f1d982186d768f9ae Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Mon, 1 Oct 2018 14:44:11 +0200 Subject: [PATCH 136/247] Async syntax (#17033) * async-syntax-mqtt_room * async-syntax-alert * Additional fixes --- homeassistant/components/alert.py | 65 ++++++++----------- .../components/binary_sensor/ffmpeg_motion.py | 2 +- .../components/binary_sensor/ffmpeg_noise.py | 2 +- homeassistant/components/sensor/mqtt_room.py | 11 ++-- 4 files changed, 33 insertions(+), 47 deletions(-) diff --git a/homeassistant/components/alert.py b/homeassistant/components/alert.py index 3ec01fc6ab8..68f471c33ab 100644 --- a/homeassistant/components/alert.py +++ b/homeassistant/components/alert.py @@ -98,14 +98,12 @@ def async_toggle(hass, entity_id): hass.services.async_call(DOMAIN, SERVICE_TOGGLE, data)) -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Set up the Alert component.""" alerts = config.get(DOMAIN) all_alerts = {} - @asyncio.coroutine - def async_handle_alert_service(service_call): + async def async_handle_alert_service(service_call): """Handle calls to alert services.""" alert_ids = service.extract_entity_ids(hass, service_call) @@ -113,11 +111,11 @@ def async_setup(hass, config): alert = all_alerts[alert_id] alert.async_set_context(service_call.context) if service_call.service == SERVICE_TURN_ON: - yield from alert.async_turn_on() + await alert.async_turn_on() elif service_call.service == SERVICE_TOGGLE: - yield from alert.async_toggle() + await alert.async_toggle() else: - yield from alert.async_turn_off() + await alert.async_turn_off() # Setup alerts for entity_id, alert in alerts.items(): @@ -141,7 +139,7 @@ def async_setup(hass, config): tasks = [alert.async_update_ha_state() for alert in all_alerts.values()] if tasks: - yield from asyncio.wait(tasks, loop=hass.loop) + await asyncio.wait(tasks, loop=hass.loop) return True @@ -196,17 +194,15 @@ class Alert(ToggleEntity): """Hide the alert when it is not firing.""" return not self._can_ack or not self._firing - @asyncio.coroutine - def watched_entity_change(self, entity, from_state, to_state): + async def watched_entity_change(self, entity, from_state, to_state): """Determine if the alert should start or stop.""" _LOGGER.debug("Watched entity (%s) has changed", entity) if to_state.state == self._alert_state and not self._firing: - yield from self.begin_alerting() + await self.begin_alerting() if to_state.state != self._alert_state and self._firing: - yield from self.end_alerting() + await self.end_alerting() - @asyncio.coroutine - def begin_alerting(self): + async def begin_alerting(self): """Begin the alert procedures.""" _LOGGER.debug("Beginning Alert: %s", self._name) self._ack = False @@ -214,25 +210,23 @@ class Alert(ToggleEntity): self._next_delay = 0 if not self._skip_first: - yield from self._notify() + await self._notify() else: - yield from self._schedule_notify() + await self._schedule_notify() self.async_schedule_update_ha_state() - @asyncio.coroutine - def end_alerting(self): + async def end_alerting(self): """End the alert procedures.""" _LOGGER.debug("Ending Alert: %s", self._name) self._cancel() self._ack = False self._firing = False if self._done_message and self._send_done_message: - yield from self._notify_done_message() + await self._notify_done_message() self.async_schedule_update_ha_state() - @asyncio.coroutine - def _schedule_notify(self): + async def _schedule_notify(self): """Schedule a notification.""" delay = self._delay[self._next_delay] next_msg = datetime.now() + delay @@ -240,8 +234,7 @@ class Alert(ToggleEntity): event.async_track_point_in_time(self.hass, self._notify, next_msg) self._next_delay = min(self._next_delay + 1, len(self._delay) - 1) - @asyncio.coroutine - def _notify(self, *args): + async def _notify(self, *args): """Send the alert notification.""" if not self._firing: return @@ -250,36 +243,32 @@ class Alert(ToggleEntity): _LOGGER.info("Alerting: %s", self._name) self._send_done_message = True for target in self._notifiers: - yield from self.hass.services.async_call( + await self.hass.services.async_call( 'notify', target, {'message': self._name}) - yield from self._schedule_notify() + await self._schedule_notify() - @asyncio.coroutine - def _notify_done_message(self, *args): + async def _notify_done_message(self, *args): """Send notification of complete alert.""" _LOGGER.info("Alerting: %s", self._done_message) self._send_done_message = False for target in self._notifiers: - yield from self.hass.services.async_call( + await self.hass.services.async_call( 'notify', target, {'message': self._done_message}) - @asyncio.coroutine - def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): """Async Unacknowledge alert.""" _LOGGER.debug("Reset Alert: %s", self._name) self._ack = False - yield from self.async_update_ha_state() + await self.async_update_ha_state() - @asyncio.coroutine - def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs): """Async Acknowledge alert.""" _LOGGER.debug("Acknowledged Alert: %s", self._name) self._ack = True - yield from self.async_update_ha_state() + await self.async_update_ha_state() - @asyncio.coroutine - def async_toggle(self, **kwargs): + async def async_toggle(self, **kwargs): """Async toggle alert.""" if self._ack: - return self.async_turn_on() - return self.async_turn_off() + return await self.async_turn_on() + return await self.async_turn_off() diff --git a/homeassistant/components/binary_sensor/ffmpeg_motion.py b/homeassistant/components/binary_sensor/ffmpeg_motion.py index 899d442c14e..df811d47e56 100644 --- a/homeassistant/components/binary_sensor/ffmpeg_motion.py +++ b/homeassistant/components/binary_sensor/ffmpeg_motion.py @@ -50,7 +50,7 @@ async def async_setup_platform(hass, config, async_add_entities, """Set up the FFmpeg binary motion sensor.""" manager = hass.data[DATA_FFMPEG] - if not manager.async_run_test(config.get(CONF_INPUT)): + if not await manager.async_run_test(config.get(CONF_INPUT)): return entity = FFmpegMotion(hass, manager, config) diff --git a/homeassistant/components/binary_sensor/ffmpeg_noise.py b/homeassistant/components/binary_sensor/ffmpeg_noise.py index 60eb236767a..a2625c3de8d 100644 --- a/homeassistant/components/binary_sensor/ffmpeg_noise.py +++ b/homeassistant/components/binary_sensor/ffmpeg_noise.py @@ -47,7 +47,7 @@ async def async_setup_platform(hass, config, async_add_entities, """Set up the FFmpeg noise binary sensor.""" manager = hass.data[DATA_FFMPEG] - if not manager.async_run_test(config.get(CONF_INPUT)): + if not await manager.async_run_test(config.get(CONF_INPUT)): return entity = FFmpegNoise(hass, manager, config) diff --git a/homeassistant/components/sensor/mqtt_room.py b/homeassistant/components/sensor/mqtt_room.py index e12e8e033ac..39c202ef01c 100644 --- a/homeassistant/components/sensor/mqtt_room.py +++ b/homeassistant/components/sensor/mqtt_room.py @@ -4,7 +4,6 @@ Support for MQTT room presence detection. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.mqtt_room/ """ -import asyncio import logging import json from datetime import timedelta @@ -52,9 +51,8 @@ MQTT_PAYLOAD = vol.Schema(vol.All(json.loads, vol.Schema({ }, extra=vol.ALLOW_EXTRA))) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up MQTT room Sensor.""" async_add_entities([MQTTRoomSensor( config.get(CONF_NAME), @@ -81,8 +79,7 @@ class MQTTRoomSensor(Entity): self._distance = None self._updated = None - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Subscribe to MQTT events.""" @callback def update_state(device_id, room, distance): @@ -118,7 +115,7 @@ class MQTTRoomSensor(Entity): or timediff.seconds >= self._timeout: update_state(**device) - return mqtt.async_subscribe( + return await mqtt.async_subscribe( self.hass, self._state_topic, message_received, 1) @property From 2e6346ca433aa308d1fb26c81152613faef9b2aa Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 1 Oct 2018 16:09:31 +0200 Subject: [PATCH 137/247] Break up websocket 2 (#17028) * Break up websocket 2 * Lint+Test * Lintttt * Rename --- homeassistant/components/auth/__init__.py | 10 +- .../components/auth/mfa_setup_flow.py | 10 +- homeassistant/components/camera/__init__.py | 4 +- homeassistant/components/cloud/http_api.py | 12 +- homeassistant/components/config/auth.py | 12 +- .../config/auth_provider_homeassistant.py | 205 +++++------ .../components/config/device_registry.py | 2 +- .../components/config/entity_registry.py | 12 +- homeassistant/components/frontend/__init__.py | 30 +- homeassistant/components/http/auth.py | 1 + homeassistant/components/lovelace/__init__.py | 2 +- .../components/media_player/__init__.py | 6 +- .../persistent_notification/__init__.py | 2 +- .../components/websocket_api/__init__.py | 328 +----------------- .../components/websocket_api/auth.py | 99 ++++++ .../components/websocket_api/commands.py | 18 +- .../components/websocket_api/connection.py | 78 +++++ .../components/websocket_api/const.py | 12 + .../components/websocket_api/decorators.py | 8 +- .../components/websocket_api/error.py | 8 + .../components/websocket_api/http.py | 189 ++++++++++ tests/components/conftest.py | 59 ++-- tests/components/test_panel_iframe.py | 8 +- tests/components/websocket_api/conftest.py | 7 +- tests/components/websocket_api/test_auth.py | 62 ++-- .../components/websocket_api/test_commands.py | 21 +- tests/components/websocket_api/test_init.py | 4 +- 27 files changed, 641 insertions(+), 568 deletions(-) create mode 100644 homeassistant/components/websocket_api/auth.py create mode 100644 homeassistant/components/websocket_api/connection.py create mode 100644 homeassistant/components/websocket_api/error.py create mode 100644 homeassistant/components/websocket_api/http.py diff --git a/homeassistant/components/auth/__init__.py b/homeassistant/components/auth/__init__.py index c0027fac820..58be53d4122 100644 --- a/homeassistant/components/auth/__init__.py +++ b/homeassistant/components/auth/__init__.py @@ -432,7 +432,7 @@ def websocket_current_user( """Get current user.""" enabled_modules = await hass.auth.async_get_enabled_mfa(user) - connection.send_message_outside( + connection.send_message( websocket_api.result_message(msg['id'], { 'id': user.id, 'name': user.name, @@ -467,7 +467,7 @@ def websocket_create_long_lived_access_token( access_token = hass.auth.async_create_access_token( refresh_token) - connection.send_message_outside( + connection.send_message( websocket_api.result_message(msg['id'], access_token)) hass.async_create_task( @@ -479,8 +479,8 @@ def websocket_create_long_lived_access_token( def websocket_refresh_tokens( hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg): """Return metadata of users refresh tokens.""" - current_id = connection.request.get('refresh_token_id') - connection.to_write.put_nowait(websocket_api.result_message(msg['id'], [{ + current_id = connection.refresh_token_id + connection.send_message(websocket_api.result_message(msg['id'], [{ 'id': refresh.id, 'client_id': refresh.client_id, 'client_name': refresh.client_name, @@ -508,7 +508,7 @@ def websocket_delete_refresh_token( await hass.auth.async_remove_refresh_token(refresh_token) - connection.send_message_outside( + connection.send_message( websocket_api.result_message(msg['id'], {})) hass.async_create_task( diff --git a/homeassistant/components/auth/mfa_setup_flow.py b/homeassistant/components/auth/mfa_setup_flow.py index 82eb913d890..121d95aede3 100644 --- a/homeassistant/components/auth/mfa_setup_flow.py +++ b/homeassistant/components/auth/mfa_setup_flow.py @@ -64,7 +64,7 @@ def websocket_setup_mfa( if flow_id is not None: result = await flow_manager.async_configure( flow_id, msg.get('user_input')) - connection.send_message_outside( + connection.send_message( websocket_api.result_message( msg['id'], _prepare_result_json(result))) return @@ -72,7 +72,7 @@ def websocket_setup_mfa( mfa_module_id = msg.get('mfa_module_id') mfa_module = hass.auth.get_auth_mfa_module(mfa_module_id) if mfa_module is None: - connection.send_message_outside(websocket_api.error_message( + connection.send_message(websocket_api.error_message( msg['id'], 'no_module', 'MFA module {} is not found'.format(mfa_module_id))) return @@ -80,7 +80,7 @@ def websocket_setup_mfa( result = await flow_manager.async_init( mfa_module_id, data={'user_id': connection.user.id}) - connection.send_message_outside( + connection.send_message( websocket_api.result_message( msg['id'], _prepare_result_json(result))) @@ -99,13 +99,13 @@ def websocket_depose_mfa( await hass.auth.async_disable_user_mfa( connection.user, msg['mfa_module_id']) except ValueError as err: - connection.send_message_outside(websocket_api.error_message( + connection.send_message(websocket_api.error_message( msg['id'], 'disable_failed', 'Cannot disable MFA Module {}: {}'.format( mfa_module_id, err))) return - connection.send_message_outside( + connection.send_message( websocket_api.result_message( msg['id'], 'done')) diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index 95f0cddf320..2cf23e0d60c 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -460,14 +460,14 @@ async def websocket_camera_thumbnail(hass, connection, msg): """ try: image = await async_get_image(hass, msg['entity_id']) - connection.send_message_outside(websocket_api.result_message( + connection.send_message(websocket_api.result_message( msg['id'], { 'content_type': image.content_type, 'content': base64.b64encode(image.content).decode('utf-8') } )) except HomeAssistantError: - connection.send_message_outside(websocket_api.error_message( + connection.send_message(websocket_api.error_message( msg['id'], 'image_fetch_failed', 'Unable to fetch image')) diff --git a/homeassistant/components/cloud/http_api.py b/homeassistant/components/cloud/http_api.py index c81ec38bace..720ca00cf52 100644 --- a/homeassistant/components/cloud/http_api.py +++ b/homeassistant/components/cloud/http_api.py @@ -231,7 +231,7 @@ def websocket_cloud_status(hass, connection, msg): Async friendly. """ cloud = hass.data[DOMAIN] - connection.to_write.put_nowait( + connection.send_message( websocket_api.result_message(msg['id'], _account_data(cloud))) @@ -241,7 +241,7 @@ async def websocket_subscription(hass, connection, msg): cloud = hass.data[DOMAIN] if not cloud.is_logged_in: - connection.to_write.put_nowait(websocket_api.error_message( + connection.send_message(websocket_api.error_message( msg['id'], 'not_logged_in', 'You need to be logged in to the cloud.')) return @@ -250,10 +250,10 @@ async def websocket_subscription(hass, connection, msg): response = await cloud.fetch_subscription_info() if response.status == 200: - connection.send_message_outside(websocket_api.result_message( + connection.send_message(websocket_api.result_message( msg['id'], await response.json())) else: - connection.send_message_outside(websocket_api.error_message( + connection.send_message(websocket_api.error_message( msg['id'], 'request_failed', 'Failed to request subscription')) @@ -263,7 +263,7 @@ async def websocket_update_prefs(hass, connection, msg): cloud = hass.data[DOMAIN] if not cloud.is_logged_in: - connection.to_write.put_nowait(websocket_api.error_message( + connection.send_message(websocket_api.error_message( msg['id'], 'not_logged_in', 'You need to be logged in to the cloud.')) return @@ -273,7 +273,7 @@ async def websocket_update_prefs(hass, connection, msg): changes.pop('type') await cloud.update_preferences(**changes) - connection.send_message_outside(websocket_api.result_message( + connection.send_message(websocket_api.result_message( msg['id'], {'success': True})) diff --git a/homeassistant/components/config/auth.py b/homeassistant/components/config/auth.py index 17dd132d4b4..f2af6589f11 100644 --- a/homeassistant/components/config/auth.py +++ b/homeassistant/components/config/auth.py @@ -49,7 +49,7 @@ def websocket_list(hass, connection, msg): """Send users.""" result = [_user_info(u) for u in await hass.auth.async_get_users()] - connection.send_message_outside( + connection.send_message( websocket_api.result_message(msg['id'], result)) hass.async_add_job(send_users()) @@ -61,8 +61,8 @@ def websocket_delete(hass, connection, msg): """Delete a user.""" async def delete_user(): """Delete user.""" - if msg['user_id'] == connection.request.get('hass_user').id: - connection.send_message_outside(websocket_api.error_message( + if msg['user_id'] == connection.user.id: + connection.send_message(websocket_api.error_message( msg['id'], 'no_delete_self', 'Unable to delete your own account')) return @@ -70,13 +70,13 @@ def websocket_delete(hass, connection, msg): user = await hass.auth.async_get_user(msg['user_id']) if not user: - connection.send_message_outside(websocket_api.error_message( + connection.send_message(websocket_api.error_message( msg['id'], 'not_found', 'User not found')) return await hass.auth.async_remove_user(user) - connection.send_message_outside( + connection.send_message( websocket_api.result_message(msg['id'])) hass.async_add_job(delete_user()) @@ -90,7 +90,7 @@ def websocket_create(hass, connection, msg): """Create a user.""" user = await hass.auth.async_create_user(msg['name']) - connection.send_message_outside( + connection.send_message( websocket_api.result_message(msg['id'], { 'user': _user_info(user) })) diff --git a/homeassistant/components/config/auth_provider_homeassistant.py b/homeassistant/components/config/auth_provider_homeassistant.py index 8f0c969a808..3495a959f49 100644 --- a/homeassistant/components/config/auth_provider_homeassistant.py +++ b/homeassistant/components/config/auth_provider_homeassistant.py @@ -2,7 +2,6 @@ import voluptuous as vol from homeassistant.auth.providers import homeassistant as auth_ha -from homeassistant.core import callback from homeassistant.components import websocket_api from homeassistant.components.websocket_api.decorators import require_owner @@ -55,121 +54,109 @@ def _get_provider(hass): raise RuntimeError('Provider not found') -@callback @require_owner -def websocket_create(hass, connection, msg): +@websocket_api.async_response +async def websocket_create(hass, connection, msg): """Create credentials and attach to a user.""" - async def create_creds(): - """Create credentials.""" - provider = _get_provider(hass) - await provider.async_initialize() + provider = _get_provider(hass) + await provider.async_initialize() - user = await hass.auth.async_get_user(msg['user_id']) + user = await hass.auth.async_get_user(msg['user_id']) - if user is None: - connection.send_message_outside(websocket_api.error_message( - msg['id'], 'not_found', 'User not found')) - return + if user is None: + connection.send_message(websocket_api.error_message( + msg['id'], 'not_found', 'User not found')) + return - if user.system_generated: - connection.send_message_outside(websocket_api.error_message( - msg['id'], 'system_generated', - 'Cannot add credentials to a system generated user.')) - return - - try: - await hass.async_add_executor_job( - provider.data.add_auth, msg['username'], msg['password']) - except auth_ha.InvalidUser: - connection.send_message_outside(websocket_api.error_message( - msg['id'], 'username_exists', 'Username already exists')) - return - - credentials = await provider.async_get_or_create_credentials({ - 'username': msg['username'] - }) - await hass.auth.async_link_user(user, credentials) - - await provider.data.async_save() - connection.to_write.put_nowait(websocket_api.result_message(msg['id'])) - - hass.async_add_job(create_creds()) - - -@callback -@require_owner -def websocket_delete(hass, connection, msg): - """Delete username and related credential.""" - async def delete_creds(): - """Delete user credentials.""" - provider = _get_provider(hass) - await provider.async_initialize() - - credentials = await provider.async_get_or_create_credentials({ - 'username': msg['username'] - }) - - # if not new, an existing credential exists. - # Removing the credential will also remove the auth. - if not credentials.is_new: - await hass.auth.async_remove_credentials(credentials) - - connection.to_write.put_nowait( - websocket_api.result_message(msg['id'])) - return - - try: - provider.data.async_remove_auth(msg['username']) - await provider.data.async_save() - except auth_ha.InvalidUser: - connection.to_write.put_nowait(websocket_api.error_message( - msg['id'], 'auth_not_found', 'Given username was not found.')) - return - - connection.to_write.put_nowait( - websocket_api.result_message(msg['id'])) - - hass.async_add_job(delete_creds()) - - -@callback -def websocket_change_password(hass, connection, msg): - """Change user password.""" - async def change_password(): - """Change user password.""" - user = connection.request.get('hass_user') - if user is None: - connection.send_message_outside(websocket_api.error_message( - msg['id'], 'user_not_found', 'User not found')) - return - - provider = _get_provider(hass) - await provider.async_initialize() - - username = None - for credential in user.credentials: - if credential.auth_provider_type == provider.type: - username = credential.data['username'] - break - - if username is None: - connection.send_message_outside(websocket_api.error_message( - msg['id'], 'credentials_not_found', 'Credentials not found')) - return - - try: - await provider.async_validate_login( - username, msg['current_password']) - except auth_ha.InvalidAuth: - connection.send_message_outside(websocket_api.error_message( - msg['id'], 'invalid_password', 'Invalid password')) - return + if user.system_generated: + connection.send_message(websocket_api.error_message( + msg['id'], 'system_generated', + 'Cannot add credentials to a system generated user.')) + return + try: await hass.async_add_executor_job( - provider.data.change_password, username, msg['new_password']) - await provider.data.async_save() + provider.data.add_auth, msg['username'], msg['password']) + except auth_ha.InvalidUser: + connection.send_message(websocket_api.error_message( + msg['id'], 'username_exists', 'Username already exists')) + return - connection.send_message_outside( + credentials = await provider.async_get_or_create_credentials({ + 'username': msg['username'] + }) + await hass.auth.async_link_user(user, credentials) + + await provider.data.async_save() + connection.send_message(websocket_api.result_message(msg['id'])) + + +@require_owner +@websocket_api.async_response +async def websocket_delete(hass, connection, msg): + """Delete username and related credential.""" + provider = _get_provider(hass) + await provider.async_initialize() + + credentials = await provider.async_get_or_create_credentials({ + 'username': msg['username'] + }) + + # if not new, an existing credential exists. + # Removing the credential will also remove the auth. + if not credentials.is_new: + await hass.auth.async_remove_credentials(credentials) + + connection.send_message( websocket_api.result_message(msg['id'])) + return - hass.async_add_job(change_password()) + try: + provider.data.async_remove_auth(msg['username']) + await provider.data.async_save() + except auth_ha.InvalidUser: + connection.send_message(websocket_api.error_message( + msg['id'], 'auth_not_found', 'Given username was not found.')) + return + + connection.send_message( + websocket_api.result_message(msg['id'])) + + +@websocket_api.async_response +async def websocket_change_password(hass, connection, msg): + """Change user password.""" + user = connection.user + if user is None: + connection.send_message(websocket_api.error_message( + msg['id'], 'user_not_found', 'User not found')) + return + + provider = _get_provider(hass) + await provider.async_initialize() + + username = None + for credential in user.credentials: + if credential.auth_provider_type == provider.type: + username = credential.data['username'] + break + + if username is None: + connection.send_message(websocket_api.error_message( + msg['id'], 'credentials_not_found', 'Credentials not found')) + return + + try: + await provider.async_validate_login( + username, msg['current_password']) + except auth_ha.InvalidAuth: + connection.send_message(websocket_api.error_message( + msg['id'], 'invalid_password', 'Invalid password')) + return + + await hass.async_add_executor_job( + provider.data.change_password, username, msg['new_password']) + await provider.data.async_save() + + connection.send_message( + websocket_api.result_message(msg['id'])) diff --git a/homeassistant/components/config/device_registry.py b/homeassistant/components/config/device_registry.py index 88aa5727a97..54396f8956c 100644 --- a/homeassistant/components/config/device_registry.py +++ b/homeassistant/components/config/device_registry.py @@ -31,7 +31,7 @@ def websocket_list_devices(hass, connection, msg): async def retrieve_entities(): """Get devices from registry.""" registry = await async_get_registry(hass) - connection.send_message_outside(websocket_api.result_message( + connection.send_message(websocket_api.result_message( msg['id'], [{ 'config_entries': list(entry.config_entries), 'connections': list(entry.connections), diff --git a/homeassistant/components/config/entity_registry.py b/homeassistant/components/config/entity_registry.py index 18d66ec623a..1ede76d0fd8 100644 --- a/homeassistant/components/config/entity_registry.py +++ b/homeassistant/components/config/entity_registry.py @@ -55,7 +55,7 @@ async def websocket_list_entities(hass, connection, msg): Async friendly. """ registry = await async_get_registry(hass) - connection.send_message_outside(websocket_api.result_message( + connection.send_message(websocket_api.result_message( msg['id'], [{ 'config_entry_id': entry.config_entry_id, 'device_id': entry.device_id, @@ -77,11 +77,11 @@ async def websocket_get_entity(hass, connection, msg): entry = registry.entities.get(msg['entity_id']) if entry is None: - connection.send_message_outside(websocket_api.error_message( + connection.send_message(websocket_api.error_message( msg['id'], ERR_NOT_FOUND, 'Entity not found')) return - connection.send_message_outside(websocket_api.result_message( + connection.send_message(websocket_api.result_message( msg['id'], _entry_dict(entry) )) @@ -95,7 +95,7 @@ async def websocket_update_entity(hass, connection, msg): registry = await async_get_registry(hass) if msg['entity_id'] not in registry.entities: - connection.send_message_outside(websocket_api.error_message( + connection.send_message(websocket_api.error_message( msg['id'], ERR_NOT_FOUND, 'Entity not found')) return @@ -112,11 +112,11 @@ async def websocket_update_entity(hass, connection, msg): entry = registry.async_update_entity( msg['entity_id'], **changes) except ValueError as err: - connection.send_message_outside(websocket_api.error_message( + connection.send_message(websocket_api.error_message( msg['id'], 'invalid_info', str(err) )) else: - connection.send_message_outside(websocket_api.result_message( + connection.send_message(websocket_api.result_message( msg['id'], _entry_dict(entry) )) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 023e75aac85..083f1a5f0d5 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -145,7 +145,7 @@ class Panel: index_view.get) @callback - def to_response(self, hass, request): + def to_response(self): """Panel as dictionary.""" return { 'component_name': self.component_name, @@ -485,12 +485,10 @@ def websocket_get_panels(hass, connection, msg): Async friendly. """ panels = { - panel: - connection.hass.data[DATA_PANELS][panel].to_response( - connection.hass, connection.request) + panel: connection.hass.data[DATA_PANELS][panel].to_response() for panel in connection.hass.data[DATA_PANELS]} - connection.to_write.put_nowait(websocket_api.result_message( + connection.send_message(websocket_api.result_message( msg['id'], panels)) @@ -500,25 +498,21 @@ def websocket_get_themes(hass, connection, msg): Async friendly. """ - connection.to_write.put_nowait(websocket_api.result_message(msg['id'], { + connection.send_message(websocket_api.result_message(msg['id'], { 'themes': hass.data[DATA_THEMES], 'default_theme': hass.data[DATA_DEFAULT_THEME], })) -@callback -def websocket_get_translations(hass, connection, msg): +@websocket_api.async_response +async def websocket_get_translations(hass, connection, msg): """Handle get translations command. Async friendly. """ - async def send_translations(): - """Send a translation.""" - resources = await async_get_translations(hass, msg['language']) - connection.send_message_outside(websocket_api.result_message( - msg['id'], { - 'resources': resources, - } - )) - - hass.async_add_job(send_translations()) + resources = await async_get_translations(hass, msg['language']) + connection.send_message(websocket_api.result_message( + msg['id'], { + 'resources': resources, + } + )) diff --git a/homeassistant/components/http/auth.py b/homeassistant/components/http/auth.py index a18b4de7a10..bcc86b36dbe 100644 --- a/homeassistant/components/http/auth.py +++ b/homeassistant/components/http/auth.py @@ -112,6 +112,7 @@ async def async_validate_auth_header(request, api_password=None): if refresh_token is None: return False + request['hass_refresh_token'] = refresh_token request['hass_user'] = refresh_token.user return True diff --git a/homeassistant/components/lovelace/__init__.py b/homeassistant/components/lovelace/__init__.py index eba69159048..a24c8eb9e91 100644 --- a/homeassistant/components/lovelace/__init__.py +++ b/homeassistant/components/lovelace/__init__.py @@ -48,4 +48,4 @@ async def websocket_lovelace_config(hass, connection, msg): if error is not None: message = websocket_api.error_message(msg['id'], *error) - connection.send_message_outside(message) + connection.send_message(message) diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 85016df7262..8530a01d3e6 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -874,19 +874,19 @@ async def websocket_handle_thumbnail(hass, connection, msg): player = component.get_entity(msg['entity_id']) if player is None: - connection.send_message_outside(websocket_api.error_message( + connection.send_message(websocket_api.error_message( msg['id'], 'entity_not_found', 'Entity not found')) return data, content_type = await player.async_get_media_image() if data is None: - connection.send_message_outside(websocket_api.error_message( + connection.send_message(websocket_api.error_message( msg['id'], 'thumbnail_fetch_failed', 'Failed to fetch thumbnail')) return - connection.send_message_outside(websocket_api.result_message( + connection.send_message(websocket_api.result_message( msg['id'], { 'content_type': content_type, 'content': base64.b64encode(data).decode('utf-8') diff --git a/homeassistant/components/persistent_notification/__init__.py b/homeassistant/components/persistent_notification/__init__.py index 066afe1fe22..a0f5cdae24d 100644 --- a/homeassistant/components/persistent_notification/__init__.py +++ b/homeassistant/components/persistent_notification/__init__.py @@ -199,7 +199,7 @@ async def async_setup(hass: HomeAssistant, config: dict) -> Awaitable[bool]: def websocket_get_notifications( hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg): """Return a list of persistent_notifications.""" - connection.to_write.put_nowait( + connection.send_message( websocket_api.result_message(msg['id'], [ { key: data[key] for key in (ATTR_NOTIFICATION_ID, ATTR_MESSAGE, diff --git a/homeassistant/components/websocket_api/__init__.py b/homeassistant/components/websocket_api/__init__.py index 448256e31fd..41d0efaf3aa 100644 --- a/homeassistant/components/websocket_api/__init__.py +++ b/homeassistant/components/websocket_api/__init__.py @@ -4,49 +4,18 @@ Websocket based API for Home Assistant. For more details about this component, please refer to the documentation at https://developers.home-assistant.io/docs/external_api_websocket.html """ -import asyncio -from concurrent import futures -from contextlib import suppress -from functools import partial -import json -import logging - -from aiohttp import web -import voluptuous as vol -from voluptuous.humanize import humanize_error - -from homeassistant.const import EVENT_HOMEASSISTANT_STOP, __version__ -from homeassistant.core import Context, callback +from homeassistant.core import callback from homeassistant.loader import bind_hass -from homeassistant.helpers.json import JSONEncoder -from homeassistant.components.http import HomeAssistantView -from homeassistant.components.http.auth import validate_password -from homeassistant.components.http.const import KEY_AUTHENTICATED -from homeassistant.components.http.ban import process_wrong_login, \ - process_success_login -from . import commands, const, decorators, messages +from . import commands, connection, const, decorators, http, messages -DOMAIN = 'websocket_api' +DOMAIN = const.DOMAIN -URL = '/api/websocket' DEPENDENCIES = ('http',) -MAX_PENDING_MSG = 512 - - -_LOGGER = logging.getLogger(__name__) - -JSON_DUMP = partial(json.dumps, cls=JSONEncoder) - -TYPE_AUTH = 'auth' -TYPE_AUTH_INVALID = 'auth_invalid' -TYPE_AUTH_OK = 'auth_ok' -TYPE_AUTH_REQUIRED = 'auth_required' - - -# Backwards compat +# Backwards compat / Make it easier to integrate # pylint: disable=invalid-name +ActiveConnection = connection.ActiveConnection BASE_COMMAND_MESSAGE_SCHEMA = messages.BASE_COMMAND_MESSAGE_SCHEMA error_message = messages.error_message result_message = messages.result_message @@ -54,42 +23,6 @@ async_response = decorators.async_response ws_require_user = decorators.ws_require_user # pylint: enable=invalid-name -AUTH_MESSAGE_SCHEMA = vol.Schema({ - vol.Required('type'): TYPE_AUTH, - vol.Exclusive('api_password', 'auth'): str, - vol.Exclusive('access_token', 'auth'): str, -}) - - -# Define the possible errors that occur when connections are cancelled. -# Originally, this was just asyncio.CancelledError, but issue #9546 showed -# that futures.CancelledErrors can also occur in some situations. -CANCELLATION_ERRORS = (asyncio.CancelledError, futures.CancelledError) - - -def auth_ok_message(): - """Return an auth_ok message.""" - return { - 'type': TYPE_AUTH_OK, - 'ha_version': __version__, - } - - -def auth_required_message(): - """Return an auth_required message.""" - return { - 'type': TYPE_AUTH_REQUIRED, - 'ha_version': __version__, - } - - -def auth_invalid_message(message): - """Return an auth_invalid message.""" - return { - 'type': TYPE_AUTH_INVALID, - 'message': message, - } - @bind_hass @callback @@ -103,255 +36,6 @@ def async_register_command(hass, command, handler, schema): async def async_setup(hass, config): """Initialize the websocket API.""" - hass.http.register_view(WebsocketAPIView) + hass.http.register_view(http.WebsocketAPIView) commands.async_register_commands(hass) return True - - -class WebsocketAPIView(HomeAssistantView): - """View to serve a websockets endpoint.""" - - name = "websocketapi" - url = URL - requires_auth = False - - async def get(self, request): - """Handle an incoming websocket connection.""" - return await ActiveConnection(request.app['hass'], request).handle() - - -class ActiveConnection: - """Handle an active websocket client connection.""" - - def __init__(self, hass, request): - """Initialize an active connection.""" - self.hass = hass - self.request = request - self.wsock = None - self.event_listeners = {} - self.to_write = asyncio.Queue(maxsize=MAX_PENDING_MSG, loop=hass.loop) - self._handle_task = None - self._writer_task = None - - @property - def user(self): - """Return the user associated with the connection.""" - return self.request.get('hass_user') - - def context(self, msg): - """Return a context.""" - user = self.user - if user is None: - return Context() - return Context(user_id=user.id) - - def debug(self, message1, message2=''): - """Print a debug message.""" - _LOGGER.debug("WS %s: %s %s", id(self.wsock), message1, message2) - - def log_error(self, message1, message2=''): - """Print an error message.""" - _LOGGER.error("WS %s: %s %s", id(self.wsock), message1, message2) - - async def _writer(self): - """Write outgoing messages.""" - # Exceptions if Socket disconnected or cancelled by connection handler - with suppress(RuntimeError, *CANCELLATION_ERRORS): - while not self.wsock.closed: - message = await self.to_write.get() - if message is None: - break - self.debug("Sending", message) - try: - await self.wsock.send_json(message, dumps=JSON_DUMP) - except TypeError as err: - _LOGGER.error('Unable to serialize to JSON: %s\n%s', - err, message) - - @callback - def send_message_outside(self, message): - """Send a message to the client. - - Closes connection if the client is not reading the messages. - - Async friendly. - """ - try: - self.to_write.put_nowait(message) - except asyncio.QueueFull: - self.log_error("Client exceeded max pending messages [2]:", - MAX_PENDING_MSG) - self.cancel() - - @callback - def cancel(self): - """Cancel the connection.""" - self._handle_task.cancel() - self._writer_task.cancel() - - async def handle(self): - """Handle the websocket connection.""" - request = self.request - wsock = self.wsock = web.WebSocketResponse(heartbeat=55) - await wsock.prepare(request) - self.debug("Connected") - - self._handle_task = asyncio.Task.current_task(loop=self.hass.loop) - - @callback - def handle_hass_stop(event): - """Cancel this connection.""" - self.cancel() - - unsub_stop = self.hass.bus.async_listen( - EVENT_HOMEASSISTANT_STOP, handle_hass_stop) - self._writer_task = self.hass.async_add_job(self._writer()) - final_message = None - msg = None - authenticated = False - - try: - if request[KEY_AUTHENTICATED]: - authenticated = True - - # always request auth when auth is active - # even request passed pre-authentication (trusted networks) - # or when using legacy api_password - if self.hass.auth.active or not authenticated: - self.debug("Request auth") - await self.wsock.send_json(auth_required_message()) - msg = await wsock.receive_json() - msg = AUTH_MESSAGE_SCHEMA(msg) - - if self.hass.auth.active and 'access_token' in msg: - self.debug("Received access_token") - refresh_token = \ - await self.hass.auth.async_validate_access_token( - msg['access_token']) - authenticated = refresh_token is not None - if authenticated: - request['hass_user'] = refresh_token.user - request['refresh_token_id'] = refresh_token.id - - elif ((not self.hass.auth.active or - self.hass.auth.support_legacy) and - 'api_password' in msg): - self.debug("Received api_password") - authenticated = validate_password( - request, msg['api_password']) - - if not authenticated: - self.debug("Authorization failed") - await self.wsock.send_json( - auth_invalid_message('Invalid access token or password')) - await process_wrong_login(request) - return wsock - - self.debug("Auth OK") - await process_success_login(request) - await self.wsock.send_json(auth_ok_message()) - - # ---------- AUTH PHASE OVER ---------- - - msg = await wsock.receive_json() - last_id = 0 - handlers = self.hass.data[DOMAIN] - - while msg: - self.debug("Received", msg) - msg = messages.MINIMAL_MESSAGE_SCHEMA(msg) - cur_id = msg['id'] - - if cur_id <= last_id: - self.to_write.put_nowait(messages.error_message( - cur_id, const.ERR_ID_REUSE, - 'Identifier values have to increase.')) - - elif msg['type'] not in handlers: - self.log_error( - 'Received invalid command: {}'.format(msg['type'])) - self.to_write.put_nowait(messages.error_message( - cur_id, const.ERR_UNKNOWN_COMMAND, - 'Unknown command.')) - - else: - handler, schema = handlers[msg['type']] - try: - handler(self.hass, self, schema(msg)) - except Exception: # pylint: disable=broad-except - _LOGGER.exception('Error handling message: %s', msg) - self.to_write.put_nowait(messages.error_message( - cur_id, const.ERR_UNKNOWN_ERROR, - 'Unknown error.')) - - last_id = cur_id - msg = await wsock.receive_json() - - except vol.Invalid as err: - error_msg = "Message incorrectly formatted: " - if msg: - error_msg += humanize_error(msg, err) - else: - error_msg += str(err) - - self.log_error(error_msg) - - if not authenticated: - final_message = auth_invalid_message(error_msg) - - else: - if isinstance(msg, dict): - iden = msg.get('id') - else: - iden = None - - final_message = messages.error_message( - iden, const.ERR_INVALID_FORMAT, error_msg) - - except TypeError as err: - if wsock.closed: - self.debug("Connection closed by client") - else: - _LOGGER.exception("Unexpected TypeError: %s", err) - - except ValueError as err: - msg = "Received invalid JSON" - value = getattr(err, 'doc', None) # Py3.5+ only - if value: - msg += ': {}'.format(value) - self.log_error(msg) - self._writer_task.cancel() - - except CANCELLATION_ERRORS: - self.debug("Connection cancelled") - - except asyncio.QueueFull: - self.log_error("Client exceeded max pending messages [1]:", - MAX_PENDING_MSG) - self._writer_task.cancel() - - except Exception: # pylint: disable=broad-except - error = "Unexpected error inside websocket API. " - if msg is not None: - error += str(msg) - _LOGGER.exception(error) - - finally: - unsub_stop() - - for unsub in self.event_listeners.values(): - unsub() - - try: - if final_message is not None: - self.to_write.put_nowait(final_message) - self.to_write.put_nowait(None) - # Make sure all error messages are written before closing - await self._writer_task - except asyncio.QueueFull: - self._writer_task.cancel() - - await wsock.close() - self.debug("Closed connection") - - return wsock diff --git a/homeassistant/components/websocket_api/auth.py b/homeassistant/components/websocket_api/auth.py new file mode 100644 index 00000000000..db41f3df06d --- /dev/null +++ b/homeassistant/components/websocket_api/auth.py @@ -0,0 +1,99 @@ +"""Handle the auth of a connection.""" +import voluptuous as vol +from voluptuous.humanize import humanize_error + +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 .connection import ActiveConnection +from .error import Disconnect + +TYPE_AUTH = 'auth' +TYPE_AUTH_INVALID = 'auth_invalid' +TYPE_AUTH_OK = 'auth_ok' +TYPE_AUTH_REQUIRED = 'auth_required' + +AUTH_MESSAGE_SCHEMA = vol.Schema({ + vol.Required('type'): TYPE_AUTH, + vol.Exclusive('api_password', 'auth'): str, + vol.Exclusive('access_token', 'auth'): str, +}) + + +def auth_ok_message(): + """Return an auth_ok message.""" + return { + 'type': TYPE_AUTH_OK, + 'ha_version': __version__, + } + + +def auth_required_message(): + """Return an auth_required message.""" + return { + 'type': TYPE_AUTH_REQUIRED, + 'ha_version': __version__, + } + + +def auth_invalid_message(message): + """Return an auth_invalid message.""" + return { + 'type': TYPE_AUTH_INVALID, + 'message': message, + } + + +class AuthPhase: + """Connection that requires client to authenticate first.""" + + def __init__(self, logger, hass, send_message, request): + """Initialize the authentiated connection.""" + self._hass = hass + self._send_message = send_message + self._logger = logger + self._request = request + self._authenticated = False + self._connection = None + + async def async_handle(self, msg): + """Handle authentication.""" + try: + msg = AUTH_MESSAGE_SCHEMA(msg) + except vol.Invalid as err: + error_msg = 'Auth message incorrectly formatted: {}'.format( + humanize_error(msg, err)) + self._logger.warning(error_msg) + self._send_message(auth_invalid_message(error_msg)) + raise Disconnect + + if self._hass.auth.active and 'access_token' in msg: + self._logger.debug("Received access_token") + refresh_token = \ + await self._hass.auth.async_validate_access_token( + msg['access_token']) + if refresh_token is not None: + return await self._async_finish_auth( + refresh_token.user, refresh_token) + + elif ((not self._hass.auth.active or 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) + + self._send_message(auth_invalid_message( + 'Invalid access token or password')) + await process_wrong_login(self._request) + raise Disconnect + + async def _async_finish_auth(self, user, refresh_token) \ + -> ActiveConnection: + """Create an active connection.""" + self._logger.debug("Auth OK") + await process_success_login(self._request) + self._send_message(auth_ok_message()) + return ActiveConnection( + self._logger, self._hass, self._send_message, user, refresh_token) diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py index c9808f3a692..8e1dac4af8e 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -103,12 +103,12 @@ def handle_subscribe_events(hass, connection, msg): if event.event_type == EVENT_TIME_CHANGED: return - connection.send_message_outside(event_message(msg['id'], event)) + connection.send_message(event_message(msg['id'], event)) connection.event_listeners[msg['id']] = hass.bus.async_listen( msg['event_type'], forward_events) - connection.to_write.put_nowait(messages.result_message(msg['id'])) + connection.send_message(messages.result_message(msg['id'])) @callback @@ -121,9 +121,9 @@ def handle_unsubscribe_events(hass, connection, msg): if subscription in connection.event_listeners: connection.event_listeners.pop(subscription)() - connection.to_write.put_nowait(messages.result_message(msg['id'])) + connection.send_message(messages.result_message(msg['id'])) else: - connection.to_write.put_nowait(messages.error_message( + connection.send_message(messages.error_message( msg['id'], const.ERR_NOT_FOUND, 'Subscription not found.')) @@ -140,7 +140,7 @@ async def handle_call_service(hass, connection, msg): await hass.services.async_call( msg['domain'], msg['service'], msg.get('service_data'), blocking, connection.context(msg)) - connection.send_message_outside(messages.result_message(msg['id'])) + connection.send_message(messages.result_message(msg['id'])) @callback @@ -149,7 +149,7 @@ def handle_get_states(hass, connection, msg): Async friendly. """ - connection.to_write.put_nowait(messages.result_message( + connection.send_message(messages.result_message( msg['id'], hass.states.async_all())) @@ -160,7 +160,7 @@ async def handle_get_services(hass, connection, msg): Async friendly. """ descriptions = await async_get_all_descriptions(hass) - connection.send_message_outside( + connection.send_message( messages.result_message(msg['id'], descriptions)) @@ -170,7 +170,7 @@ def handle_get_config(hass, connection, msg): Async friendly. """ - connection.to_write.put_nowait(messages.result_message( + connection.send_message(messages.result_message( msg['id'], hass.config.as_dict())) @@ -180,4 +180,4 @@ def handle_ping(hass, connection, msg): Async friendly. """ - connection.to_write.put_nowait(pong_message(msg['id'])) + connection.send_message(pong_message(msg['id'])) diff --git a/homeassistant/components/websocket_api/connection.py b/homeassistant/components/websocket_api/connection.py new file mode 100644 index 00000000000..1cb58591a0a --- /dev/null +++ b/homeassistant/components/websocket_api/connection.py @@ -0,0 +1,78 @@ +"""Connection session.""" +import voluptuous as vol + +from homeassistant.core import callback, Context + +from . import const, messages + + +class ActiveConnection: + """Handle an active websocket client connection.""" + + def __init__(self, logger, hass, send_message, user, refresh_token): + """Initialize an active connection.""" + self.logger = logger + self.hass = hass + self.send_message = send_message + self.user = user + if refresh_token: + self.refresh_token_id = refresh_token.id + else: + self.refresh_token_id = None + + self.event_listeners = {} + self.last_id = 0 + + def context(self, msg): + """Return a context.""" + user = self.user + if user is None: + return Context() + return Context(user_id=user.id) + + @callback + def async_handle(self, msg): + """Handle a single incoming message.""" + handlers = self.hass.data[const.DOMAIN] + + try: + msg = messages.MINIMAL_MESSAGE_SCHEMA(msg) + cur_id = msg['id'] + except vol.Invalid: + self.logger.error('Received invalid command', msg) + self.send_message(messages.error_message( + msg.get('id'), const.ERR_INVALID_FORMAT, + 'Message incorrectly formatted.')) + return + + if cur_id <= self.last_id: + self.send_message(messages.error_message( + cur_id, const.ERR_ID_REUSE, + 'Identifier values have to increase.')) + return + + if msg['type'] not in handlers: + self.logger.error( + 'Received invalid command: {}'.format(msg['type'])) + self.send_message(messages.error_message( + cur_id, const.ERR_UNKNOWN_COMMAND, + 'Unknown command.')) + return + + handler, schema = handlers[msg['type']] + + try: + handler(self.hass, self, schema(msg)) + except Exception: # pylint: disable=broad-except + self.logger.exception('Error handling message: %s', msg) + self.send_message(messages.error_message( + cur_id, const.ERR_UNKNOWN_ERROR, + 'Unknown error.')) + + self.last_id = cur_id + + @callback + def async_close(self): + """Close down connection.""" + for unsub in self.event_listeners.values(): + unsub() diff --git a/homeassistant/components/websocket_api/const.py b/homeassistant/components/websocket_api/const.py index cbc56b168c6..8d452959ca5 100644 --- a/homeassistant/components/websocket_api/const.py +++ b/homeassistant/components/websocket_api/const.py @@ -1,4 +1,11 @@ """Websocket constants.""" +import asyncio +from concurrent import futures + +DOMAIN = 'websocket_api' +URL = '/api/websocket' +MAX_PENDING_MSG = 512 + ERR_ID_REUSE = 1 ERR_INVALID_FORMAT = 2 ERR_NOT_FOUND = 3 @@ -6,3 +13,8 @@ ERR_UNKNOWN_COMMAND = 4 ERR_UNKNOWN_ERROR = 5 TYPE_RESULT = 'result' + +# Define the possible errors that occur when connections are cancelled. +# Originally, this was just asyncio.CancelledError, but issue #9546 showed +# that futures.CancelledErrors can also occur in some situations. +CANCELLATION_ERRORS = (asyncio.CancelledError, futures.CancelledError) diff --git a/homeassistant/components/websocket_api/decorators.py b/homeassistant/components/websocket_api/decorators.py index df32dd06d2b..aaa054e4054 100644 --- a/homeassistant/components/websocket_api/decorators.py +++ b/homeassistant/components/websocket_api/decorators.py @@ -18,7 +18,7 @@ def async_response(func): await func(hass, connection, msg) except Exception: # pylint: disable=broad-except _LOGGER.exception("Unexpected exception") - connection.send_message_outside(messages.error_message( + connection.send_message(messages.error_message( msg['id'], 'unknown', 'Unexpected error occurred')) @callback @@ -35,10 +35,10 @@ def require_owner(func): @wraps(func) def with_owner(hass, connection, msg): """Check owner and call function.""" - user = connection.request.get('hass_user') + user = connection.user if user is None or not user.is_owner: - connection.to_write.put_nowait(messages.error_message( + connection.send_message(messages.error_message( msg['id'], 'unauthorized', 'This command is for owners only.')) return @@ -61,7 +61,7 @@ def ws_require_user( """Check current user.""" def output_error(message_id, message): """Output error message.""" - connection.send_message_outside(messages.error_message( + connection.send_message(messages.error_message( msg['id'], message_id, message)) if connection.user is None: diff --git a/homeassistant/components/websocket_api/error.py b/homeassistant/components/websocket_api/error.py new file mode 100644 index 00000000000..c0b7ea04554 --- /dev/null +++ b/homeassistant/components/websocket_api/error.py @@ -0,0 +1,8 @@ +"""WebSocket API related errors.""" +from homeassistant.exceptions import HomeAssistantError + + +class Disconnect(HomeAssistantError): + """Disconnect the current session.""" + + pass diff --git a/homeassistant/components/websocket_api/http.py b/homeassistant/components/websocket_api/http.py new file mode 100644 index 00000000000..87f25c9b3ef --- /dev/null +++ b/homeassistant/components/websocket_api/http.py @@ -0,0 +1,189 @@ +"""View to accept incoming websocket connection.""" +import asyncio +from contextlib import suppress +from functools import partial +import json +import logging + +from aiohttp import web, WSMsgType +import async_timeout + +from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.core import callback +from homeassistant.components.http import HomeAssistantView +from homeassistant.helpers.json import JSONEncoder + +from .const import MAX_PENDING_MSG, CANCELLATION_ERRORS, URL +from .auth import AuthPhase, auth_required_message +from .error import Disconnect + +JSON_DUMP = partial(json.dumps, cls=JSONEncoder) + + +class WebsocketAPIView(HomeAssistantView): + """View to serve a websockets endpoint.""" + + name = "websocketapi" + url = URL + requires_auth = False + + async def get(self, request): + """Handle an incoming websocket connection.""" + return await WebSocketHandler( + request.app['hass'], request).async_handle() + + +class WebSocketHandler: + """Handle an active websocket client connection.""" + + def __init__(self, hass, request): + """Initialize an active connection.""" + self.hass = hass + self.request = request + self.wsock = None + self._to_write = asyncio.Queue(maxsize=MAX_PENDING_MSG, loop=hass.loop) + self._handle_task = None + self._writer_task = None + self._logger = logging.getLogger( + "{}.connection.{}".format(__name__, id(self))) + + async def _writer(self): + """Write outgoing messages.""" + # Exceptions if Socket disconnected or cancelled by connection handler + with suppress(RuntimeError, *CANCELLATION_ERRORS): + while not self.wsock.closed: + message = await self._to_write.get() + if message is None: + break + self._logger.debug("Sending %s", message) + try: + await self.wsock.send_json(message, dumps=JSON_DUMP) + except TypeError as err: + self._logger.error('Unable to serialize to JSON: %s\n%s', + err, message) + + @callback + def _send_message(self, message): + """Send a message to the client. + + Closes connection if the client is not reading the messages. + + Async friendly. + """ + try: + self._to_write.put_nowait(message) + except asyncio.QueueFull: + self._logger.error("Client exceeded max pending messages [2]: %s", + MAX_PENDING_MSG) + self._cancel() + + @callback + def _cancel(self): + """Cancel the connection.""" + self._handle_task.cancel() + self._writer_task.cancel() + + async def async_handle(self): + """Handle a websocket response.""" + request = self.request + wsock = self.wsock = web.WebSocketResponse(heartbeat=55) + await wsock.prepare(request) + self._logger.debug("Connected") + + # Py3.7+ + if hasattr(asyncio, 'current_task'): + # pylint: disable=no-member + self._handle_task = asyncio.current_task() + else: + self._handle_task = asyncio.Task.current_task(loop=self.hass.loop) + + @callback + def handle_hass_stop(event): + """Cancel this connection.""" + self._cancel() + + unsub_stop = self.hass.bus.async_listen( + EVENT_HOMEASSISTANT_STOP, handle_hass_stop) + + self._writer_task = self.hass.async_create_task(self._writer()) + + auth = AuthPhase(self._logger, self.hass, self._send_message, request) + connection = None + disconnect_warn = None + + try: + self._send_message(auth_required_message()) + + # Auth Phase + try: + with async_timeout.timeout(10): + msg = await wsock.receive() + except asyncio.TimeoutError: + disconnect_warn = \ + 'Did not receive auth message within 10 seconds' + raise Disconnect + + if msg.type in (WSMsgType.CLOSE, WSMsgType.CLOSING): + raise Disconnect + + elif msg.type != WSMsgType.TEXT: + disconnect_warn = 'Received non-Text message.' + raise Disconnect + + try: + msg = msg.json() + except ValueError: + disconnect_warn = 'Received invalid JSON.' + raise Disconnect + + self._logger.debug("Received %s", msg) + connection = await auth.async_handle(msg) + + # Command phase + while not wsock.closed: + msg = await wsock.receive() + + if msg.type in (WSMsgType.CLOSE, WSMsgType.CLOSING): + break + + elif msg.type != WSMsgType.TEXT: + disconnect_warn = 'Received non-Text message.' + break + + try: + msg = msg.json() + except ValueError: + disconnect_warn = 'Received invalid JSON.' + break + + self._logger.debug("Received %s", msg) + connection.async_handle(msg) + + except asyncio.CancelledError: + self._logger.info("Connection closed by client") + + except Disconnect: + pass + + except Exception: # pylint: disable=broad-except + self._logger.exception("Unexpected error inside websocket API") + + finally: + unsub_stop() + + if connection is not None: + connection.async_close() + + try: + self._to_write.put_nowait(None) + # Make sure all error messages are written before closing + await self._writer_task + except asyncio.QueueFull: + self._writer_task.cancel() + + await wsock.close() + + if disconnect_warn is None: + self._logger.debug("Disconnected") + else: + self._logger.warning("Disconnected: %s", disconnect_warn) diff --git a/tests/components/conftest.py b/tests/components/conftest.py index 232405a632c..252d0b1d872 100644 --- a/tests/components/conftest.py +++ b/tests/components/conftest.py @@ -4,7 +4,9 @@ from unittest.mock import patch import pytest from homeassistant.setup import async_setup_component -from homeassistant.components import websocket_api +from homeassistant.components.websocket_api.http import URL +from homeassistant.components.websocket_api.auth import ( + TYPE_AUTH, TYPE_AUTH_OK, TYPE_AUTH_REQUIRED) from tests.common import MockUser, CLIENT_ID @@ -14,41 +16,52 @@ def hass_ws_client(aiohttp_client): """Websocket client fixture connected to websocket server.""" async def create_client(hass, access_token=None): """Create a websocket client.""" - wapi = hass.components.websocket_api assert await async_setup_component(hass, 'websocket_api') client = await aiohttp_client(hass.http.app) - patching = None + patches = [] - if access_token is not None: - patching = patch('homeassistant.auth.AuthManager.active', - return_value=True) - patching.start() + if access_token is None: + patches.append(patch( + 'homeassistant.auth.AuthManager.active', return_value=False)) + patches.append(patch( + 'homeassistant.auth.AuthManager.support_legacy', + return_value=True)) + patches.append(patch( + 'homeassistant.components.websocket_api.auth.' + 'validate_password', return_value=True)) + else: + patches.append(patch( + 'homeassistant.auth.AuthManager.active', return_value=True)) + patches.append(patch( + 'homeassistant.components.http.auth.setup_auth')) + + for p in patches: + p.start() try: - websocket = await client.ws_connect(wapi.URL) + websocket = await client.ws_connect(URL) auth_resp = await websocket.receive_json() + assert auth_resp['type'] == TYPE_AUTH_REQUIRED - if auth_resp['type'] == wapi.TYPE_AUTH_OK: - assert access_token is None, \ - 'Access token given but no auth required' - return websocket - - assert access_token is not None, \ - 'Access token required for fixture' - - await websocket.send_json({ - 'type': websocket_api.TYPE_AUTH, - 'access_token': access_token - }) + if access_token is None: + await websocket.send_json({ + 'type': TYPE_AUTH, + 'api_password': 'bla' + }) + else: + await websocket.send_json({ + 'type': TYPE_AUTH, + 'access_token': access_token + }) auth_ok = await websocket.receive_json() - assert auth_ok['type'] == wapi.TYPE_AUTH_OK + assert auth_ok['type'] == TYPE_AUTH_OK finally: - if patching is not None: - patching.stop() + for p in patches: + p.stop() # wrap in client websocket.client = client diff --git a/tests/components/test_panel_iframe.py b/tests/components/test_panel_iframe.py index 3ac06c09a26..cb868f64b58 100644 --- a/tests/components/test_panel_iframe.py +++ b/tests/components/test_panel_iframe.py @@ -62,7 +62,7 @@ class TestPanelIframe(unittest.TestCase): panels = self.hass.data[frontend.DATA_PANELS] - assert panels.get('router').to_response(self.hass, None) == { + assert panels.get('router').to_response() == { 'component_name': 'iframe', 'config': {'url': 'http://192.168.1.1'}, 'icon': 'mdi:network-wireless', @@ -70,7 +70,7 @@ class TestPanelIframe(unittest.TestCase): 'url_path': 'router' } - assert panels.get('weather').to_response(self.hass, None) == { + assert panels.get('weather').to_response() == { 'component_name': 'iframe', 'config': {'url': 'https://www.wunderground.com/us/ca/san-diego'}, 'icon': 'mdi:weather', @@ -78,7 +78,7 @@ class TestPanelIframe(unittest.TestCase): 'url_path': 'weather', } - assert panels.get('api').to_response(self.hass, None) == { + assert panels.get('api').to_response() == { 'component_name': 'iframe', 'config': {'url': '/api'}, 'icon': 'mdi:weather', @@ -86,7 +86,7 @@ class TestPanelIframe(unittest.TestCase): 'url_path': 'api', } - assert panels.get('ftp').to_response(self.hass, None) == { + assert panels.get('ftp').to_response() == { 'component_name': 'iframe', 'config': {'url': 'ftp://some/ftp'}, 'icon': 'mdi:weather', diff --git a/tests/components/websocket_api/conftest.py b/tests/components/websocket_api/conftest.py index 063e0b43d1b..b7825600cb1 100644 --- a/tests/components/websocket_api/conftest.py +++ b/tests/components/websocket_api/conftest.py @@ -2,7 +2,8 @@ import pytest from homeassistant.setup import async_setup_component -from homeassistant.components import websocket_api as wapi +from homeassistant.components.websocket_api.http import URL +from homeassistant.components.websocket_api.auth import TYPE_AUTH_REQUIRED from . import API_PASSWORD @@ -24,10 +25,10 @@ def no_auth_websocket_client(hass, loop, aiohttp_client): })) client = loop.run_until_complete(aiohttp_client(hass.http.app)) - ws = loop.run_until_complete(client.ws_connect(wapi.URL)) + ws = loop.run_until_complete(client.ws_connect(URL)) auth_ok = loop.run_until_complete(ws.receive_json()) - assert auth_ok['type'] == wapi.TYPE_AUTH_REQUIRED + assert auth_ok['type'] == TYPE_AUTH_REQUIRED yield ws diff --git a/tests/components/websocket_api/test_auth.py b/tests/components/websocket_api/test_auth.py index ee1de906fa1..ed54b509aaa 100644 --- a/tests/components/websocket_api/test_auth.py +++ b/tests/components/websocket_api/test_auth.py @@ -1,7 +1,10 @@ """Test auth of websocket API.""" from unittest.mock import patch -from homeassistant.components import websocket_api as wapi +from homeassistant.components.websocket_api.const import URL +from homeassistant.components.websocket_api.auth import ( + TYPE_AUTH, TYPE_AUTH_INVALID, TYPE_AUTH_OK, TYPE_AUTH_REQUIRED) + from homeassistant.components.websocket_api import commands from homeassistant.setup import async_setup_component @@ -13,28 +16,29 @@ from . import API_PASSWORD async def test_auth_via_msg(no_auth_websocket_client): """Test authenticating.""" await no_auth_websocket_client.send_json({ - 'type': wapi.TYPE_AUTH, + 'type': TYPE_AUTH, 'api_password': API_PASSWORD }) msg = await no_auth_websocket_client.receive_json() - assert msg['type'] == wapi.TYPE_AUTH_OK + assert msg['type'] == TYPE_AUTH_OK async def test_auth_via_msg_incorrect_pass(no_auth_websocket_client): """Test authenticating.""" - with patch('homeassistant.components.websocket_api.process_wrong_login', - return_value=mock_coro()) as mock_process_wrong_login: + with patch('homeassistant.components.websocket_api.auth.' + 'process_wrong_login', return_value=mock_coro()) \ + as mock_process_wrong_login: await no_auth_websocket_client.send_json({ - 'type': wapi.TYPE_AUTH, + 'type': TYPE_AUTH, 'api_password': API_PASSWORD + 'wrong' }) msg = await no_auth_websocket_client.receive_json() assert mock_process_wrong_login.called - assert msg['type'] == wapi.TYPE_AUTH_INVALID + assert msg['type'] == TYPE_AUTH_INVALID assert msg['message'] == 'Invalid access token or password' @@ -51,8 +55,8 @@ async def test_pre_auth_only_auth_allowed(no_auth_websocket_client): msg = await no_auth_websocket_client.receive_json() - assert msg['type'] == wapi.TYPE_AUTH_INVALID - assert msg['message'].startswith('Message incorrectly formatted') + assert msg['type'] == TYPE_AUTH_INVALID + assert msg['message'].startswith('Auth message incorrectly formatted') async def test_auth_active_with_token(hass, aiohttp_client, hass_access_token): @@ -65,19 +69,19 @@ async def test_auth_active_with_token(hass, aiohttp_client, hass_access_token): client = await aiohttp_client(hass.http.app) - async with client.ws_connect(wapi.URL) as ws: + async with client.ws_connect(URL) as ws: with patch('homeassistant.auth.AuthManager.active') as auth_active: auth_active.return_value = True auth_msg = await ws.receive_json() - assert auth_msg['type'] == wapi.TYPE_AUTH_REQUIRED + assert auth_msg['type'] == TYPE_AUTH_REQUIRED await ws.send_json({ - 'type': wapi.TYPE_AUTH, + 'type': TYPE_AUTH, 'access_token': hass_access_token }) auth_msg = await ws.receive_json() - assert auth_msg['type'] == wapi.TYPE_AUTH_OK + assert auth_msg['type'] == TYPE_AUTH_OK async def test_auth_active_user_inactive(hass, aiohttp_client, @@ -94,19 +98,19 @@ async def test_auth_active_user_inactive(hass, aiohttp_client, client = await aiohttp_client(hass.http.app) - async with client.ws_connect(wapi.URL) as ws: + async with client.ws_connect(URL) as ws: with patch('homeassistant.auth.AuthManager.active') as auth_active: auth_active.return_value = True auth_msg = await ws.receive_json() - assert auth_msg['type'] == wapi.TYPE_AUTH_REQUIRED + assert auth_msg['type'] == TYPE_AUTH_REQUIRED await ws.send_json({ - 'type': wapi.TYPE_AUTH, + 'type': TYPE_AUTH, 'access_token': hass_access_token }) auth_msg = await ws.receive_json() - assert auth_msg['type'] == wapi.TYPE_AUTH_INVALID + assert auth_msg['type'] == TYPE_AUTH_INVALID async def test_auth_active_with_password_not_allow(hass, aiohttp_client): @@ -119,19 +123,19 @@ async def test_auth_active_with_password_not_allow(hass, aiohttp_client): client = await aiohttp_client(hass.http.app) - async with client.ws_connect(wapi.URL) as ws: + async with client.ws_connect(URL) as ws: with patch('homeassistant.auth.AuthManager.active', return_value=True): auth_msg = await ws.receive_json() - assert auth_msg['type'] == wapi.TYPE_AUTH_REQUIRED + assert auth_msg['type'] == TYPE_AUTH_REQUIRED await ws.send_json({ - 'type': wapi.TYPE_AUTH, + 'type': TYPE_AUTH, 'api_password': API_PASSWORD }) auth_msg = await ws.receive_json() - assert auth_msg['type'] == wapi.TYPE_AUTH_INVALID + assert auth_msg['type'] == TYPE_AUTH_INVALID async def test_auth_legacy_support_with_password(hass, aiohttp_client): @@ -144,21 +148,21 @@ async def test_auth_legacy_support_with_password(hass, aiohttp_client): client = await aiohttp_client(hass.http.app) - async with client.ws_connect(wapi.URL) as ws: + async with client.ws_connect(URL) as ws: with patch('homeassistant.auth.AuthManager.active', return_value=True),\ patch('homeassistant.auth.AuthManager.support_legacy', return_value=True): auth_msg = await ws.receive_json() - assert auth_msg['type'] == wapi.TYPE_AUTH_REQUIRED + assert auth_msg['type'] == TYPE_AUTH_REQUIRED await ws.send_json({ - 'type': wapi.TYPE_AUTH, + 'type': TYPE_AUTH, 'api_password': API_PASSWORD }) auth_msg = await ws.receive_json() - assert auth_msg['type'] == wapi.TYPE_AUTH_OK + assert auth_msg['type'] == TYPE_AUTH_OK async def test_auth_with_invalid_token(hass, aiohttp_client): @@ -171,16 +175,16 @@ async def test_auth_with_invalid_token(hass, aiohttp_client): client = await aiohttp_client(hass.http.app) - async with client.ws_connect(wapi.URL) as ws: + async with client.ws_connect(URL) as ws: with patch('homeassistant.auth.AuthManager.active') as auth_active: auth_active.return_value = True auth_msg = await ws.receive_json() - assert auth_msg['type'] == wapi.TYPE_AUTH_REQUIRED + assert auth_msg['type'] == TYPE_AUTH_REQUIRED await ws.send_json({ - 'type': wapi.TYPE_AUTH, + 'type': TYPE_AUTH, 'access_token': 'incorrect' }) auth_msg = await ws.receive_json() - assert auth_msg['type'] == wapi.TYPE_AUTH_INVALID + assert auth_msg['type'] == TYPE_AUTH_INVALID diff --git a/tests/components/websocket_api/test_commands.py b/tests/components/websocket_api/test_commands.py index 0eaf215afaa..84c29533859 100644 --- a/tests/components/websocket_api/test_commands.py +++ b/tests/components/websocket_api/test_commands.py @@ -4,7 +4,10 @@ from unittest.mock import patch from async_timeout import timeout from homeassistant.core import callback -from homeassistant.components import websocket_api as wapi +from homeassistant.components.websocket_api.const import URL +from homeassistant.components.websocket_api.auth import ( + TYPE_AUTH, TYPE_AUTH_OK, TYPE_AUTH_REQUIRED +) from homeassistant.components.websocket_api import const, commands from homeassistant.setup import async_setup_component @@ -178,19 +181,19 @@ async def test_call_service_context_with_user(hass, aiohttp_client, calls = async_mock_service(hass, 'domain_test', 'test_service') client = await aiohttp_client(hass.http.app) - async with client.ws_connect(wapi.URL) as ws: + async with client.ws_connect(URL) as ws: with patch('homeassistant.auth.AuthManager.active') as auth_active: auth_active.return_value = True auth_msg = await ws.receive_json() - assert auth_msg['type'] == wapi.TYPE_AUTH_REQUIRED + assert auth_msg['type'] == TYPE_AUTH_REQUIRED await ws.send_json({ - 'type': wapi.TYPE_AUTH, + 'type': TYPE_AUTH, 'access_token': hass_access_token }) auth_msg = await ws.receive_json() - assert auth_msg['type'] == wapi.TYPE_AUTH_OK + assert auth_msg['type'] == TYPE_AUTH_OK await ws.send_json({ 'id': 5, @@ -227,17 +230,17 @@ async def test_call_service_context_no_user(hass, aiohttp_client): calls = async_mock_service(hass, 'domain_test', 'test_service') client = await aiohttp_client(hass.http.app) - async with client.ws_connect(wapi.URL) as ws: + async with client.ws_connect(URL) as ws: auth_msg = await ws.receive_json() - assert auth_msg['type'] == wapi.TYPE_AUTH_REQUIRED + assert auth_msg['type'] == TYPE_AUTH_REQUIRED await ws.send_json({ - 'type': wapi.TYPE_AUTH, + 'type': TYPE_AUTH, 'api_password': API_PASSWORD }) auth_msg = await ws.receive_json() - assert auth_msg['type'] == wapi.TYPE_AUTH_OK + assert auth_msg['type'] == TYPE_AUTH_OK await ws.send_json({ 'id': 5, diff --git a/tests/components/websocket_api/test_init.py b/tests/components/websocket_api/test_init.py index 97acc1210fc..a7e54e8146a 100644 --- a/tests/components/websocket_api/test_init.py +++ b/tests/components/websocket_api/test_init.py @@ -5,14 +5,14 @@ from unittest.mock import patch, Mock from aiohttp import WSMsgType import pytest -from homeassistant.components import websocket_api as wapi from homeassistant.components.websocket_api import const, commands, messages @pytest.fixture def mock_low_queue(): """Mock a low queue.""" - with patch.object(wapi, 'MAX_PENDING_MSG', 5): + with patch('homeassistant.components.websocket_api.http.MAX_PENDING_MSG', + 5): yield From fbc1c41673cd0c1321212407da2d88a7d4d8a44f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 1 Oct 2018 16:12:25 +0200 Subject: [PATCH 138/247] Logbook context (#16937) * Convert logbook to use attr * Add context to events * Enhance logbook * Lint * Fix logbook entry * Don't use intermediary classes for logbook entries --- .../components/automation/__init__.py | 5 +- homeassistant/components/logbook.py | 111 +++++++++++------- homeassistant/scripts/benchmark/__init__.py | 2 +- tests/components/test_logbook.py | 95 ++++++++++----- 4 files changed, 133 insertions(+), 80 deletions(-) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index b34a6b7cf40..a1f1563f5e1 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -17,7 +17,6 @@ from homeassistant.loader import bind_hass from homeassistant.const import ( ATTR_ENTITY_ID, CONF_PLATFORM, STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE, SERVICE_RELOAD, EVENT_HOMEASSISTANT_START, CONF_ID) -from homeassistant.components import logbook from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import extract_domain_configs, script, condition from homeassistant.helpers.entity import ToggleEntity @@ -369,8 +368,8 @@ def _async_get_action(hass, config, name): async def action(entity_id, variables, context): """Execute an action.""" _LOGGER.info('Executing %s', name) - logbook.async_log_entry( - hass, name, 'has been triggered', DOMAIN, entity_id) + hass.components.logbook.async_log_entry( + name, 'has been triggered', DOMAIN, entity_id) await script_obj.async_run(variables, context) return action diff --git a/homeassistant/components/logbook.py b/homeassistant/components/logbook.py index c4fcf53a9c1..e282a133f1d 100644 --- a/homeassistant/components/logbook.py +++ b/homeassistant/components/logbook.py @@ -10,6 +10,7 @@ import logging import voluptuous as vol +from homeassistant.loader import bind_hass from homeassistant.components import sun from homeassistant.components.http import HomeAssistantView from homeassistant.const import ( @@ -17,8 +18,9 @@ from homeassistant.const import ( CONF_INCLUDE, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, EVENT_LOGBOOK_ENTRY, EVENT_STATE_CHANGED, HTTP_BAD_REQUEST, STATE_NOT_HOME, STATE_OFF, STATE_ON) -from homeassistant.core import DOMAIN as HA_DOMAIN -from homeassistant.core import State, callback, split_entity_id +from homeassistant.core import ( + DOMAIN as HA_DOMAIN, State, callback, split_entity_id) +from homeassistant.components.alexa.smart_home import EVENT_ALEXA_SMART_HOME import homeassistant.helpers.config_validation as cv import homeassistant.util.dt as dt_util @@ -53,7 +55,8 @@ CONFIG_SCHEMA = vol.Schema({ ALL_EVENT_TYPES = [ EVENT_STATE_CHANGED, EVENT_LOGBOOK_ENTRY, - EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP + EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, + EVENT_ALEXA_SMART_HOME ] LOG_MESSAGE_SCHEMA = vol.Schema({ @@ -64,11 +67,13 @@ LOG_MESSAGE_SCHEMA = vol.Schema({ }) +@bind_hass def log_entry(hass, name, message, domain=None, entity_id=None): """Add an entry to the logbook.""" hass.add_job(async_log_entry, hass, name, message, domain, entity_id) +@bind_hass def async_log_entry(hass, name, message, domain=None, entity_id=None): """Add an entry to the logbook.""" data = { @@ -140,30 +145,7 @@ class LogbookView(HomeAssistantView): return await hass.async_add_job(json_events) -class Entry: - """A human readable version of the log.""" - - def __init__(self, when=None, name=None, message=None, domain=None, - entity_id=None): - """Initialize the entry.""" - self.when = when - self.name = name - self.message = message - self.domain = domain - self.entity_id = entity_id - - def as_dict(self): - """Convert entry to a dict to be used within JSON.""" - return { - 'when': self.when, - 'name': self.name, - 'message': self.message, - 'domain': self.domain, - 'entity_id': self.entity_id, - } - - -def humanify(events): +def humanify(hass, events): """Generate a converted list of events into Entry objects. Will try to group events if possible: @@ -224,20 +206,28 @@ def humanify(events): to_state.attributes.get('unit_of_measurement'): continue - yield Entry( - event.time_fired, - name=to_state.name, - message=_entry_message_from_state(domain, to_state), - domain=domain, - entity_id=to_state.entity_id) + yield { + 'when': event.time_fired, + 'name': to_state.name, + 'message': _entry_message_from_state(domain, to_state), + 'domain': domain, + 'entity_id': to_state.entity_id, + 'context_id': event.context.id, + 'context_user_id': event.context.user_id + } elif event.event_type == EVENT_HOMEASSISTANT_START: if start_stop_events.get(event.time_fired.minute) == 2: continue - yield Entry( - event.time_fired, "Home Assistant", "started", - domain=HA_DOMAIN) + yield { + 'when': event.time_fired, + 'name': "Home Assistant", + 'message': "started", + 'domain': HA_DOMAIN, + 'context_id': event.context.id, + 'context_user_id': event.context.user_id + } elif event.event_type == EVENT_HOMEASSISTANT_STOP: if start_stop_events.get(event.time_fired.minute) == 2: @@ -245,9 +235,14 @@ def humanify(events): else: action = "stopped" - yield Entry( - event.time_fired, "Home Assistant", action, - domain=HA_DOMAIN) + yield { + 'when': event.time_fired, + 'name': "Home Assistant", + 'message': action, + 'domain': HA_DOMAIN, + 'context_id': event.context.id, + 'context_user_id': event.context.user_id + } elif event.event_type == EVENT_LOGBOOK_ENTRY: domain = event.data.get(ATTR_DOMAIN) @@ -258,10 +253,38 @@ def humanify(events): except IndexError: pass - yield Entry( - event.time_fired, event.data.get(ATTR_NAME), - event.data.get(ATTR_MESSAGE), domain, - entity_id) + yield { + 'when': event.time_fired, + 'name': event.data.get(ATTR_NAME), + 'message': event.data.get(ATTR_MESSAGE), + 'domain': domain, + 'entity_id': entity_id, + 'context_id': event.context.id, + 'context_user_id': event.context.user_id + } + + elif event.event_type == EVENT_ALEXA_SMART_HOME: + data = event.data + entity_id = data.get('entity_id') + + if entity_id: + state = hass.states.get(entity_id) + name = state.name if state else entity_id + message = "send command {}/{} for {}".format( + data['namespace'], data['name'], name) + else: + message = "send command {}/{}".format( + data['namespace'], data['name']) + + yield { + 'when': event.time_fired, + 'name': 'Amazon Alexa', + 'message': message, + 'domain': 'alexa', + 'entity_id': entity_id, + 'context_id': event.context.id, + 'context_user_id': event.context.user_id + } def _get_events(hass, config, start_day, end_day): @@ -279,7 +302,7 @@ def _get_events(hass, config, start_day, end_day): .filter((States.last_updated == States.last_changed) | (States.state_id.is_(None))) events = execute(query) - return humanify(_exclude_events(events, config)) + return humanify(hass, _exclude_events(events, config)) def _exclude_events(events, config): diff --git a/homeassistant/scripts/benchmark/__init__.py b/homeassistant/scripts/benchmark/__init__.py index 98de59f2da1..f0df58a51f4 100644 --- a/homeassistant/scripts/benchmark/__init__.py +++ b/homeassistant/scripts/benchmark/__init__.py @@ -186,6 +186,6 @@ def _logbook_filtering(hass, last_changed, last_updated): # pylint: disable=protected-access events = logbook._exclude_events(events, {}) - list(logbook.humanify(events)) + list(logbook.humanify(None, events)) return timer() - start diff --git a/tests/components/test_logbook.py b/tests/components/test_logbook.py index cf78fbec352..9ccb8f58a87 100644 --- a/tests/components/test_logbook.py +++ b/tests/components/test_logbook.py @@ -11,6 +11,7 @@ from homeassistant.const import ( ATTR_HIDDEN, STATE_NOT_HOME, STATE_ON, STATE_OFF) import homeassistant.util.dt as dt_util from homeassistant.components import logbook, recorder +from homeassistant.components.alexa.smart_home import EVENT_ALEXA_SMART_HOME from homeassistant.setup import setup_component, async_setup_component from tests.common import ( @@ -99,7 +100,7 @@ class TestComponentLogbook(unittest.TestCase): eventB = self.create_state_changed_event(pointB, entity_id, 20) eventC = self.create_state_changed_event(pointC, entity_id, 30) - entries = list(logbook.humanify((eventA, eventB, eventC))) + entries = list(logbook.humanify(self.hass, (eventA, eventB, eventC))) self.assertEqual(2, len(entries)) self.assert_entry( @@ -116,7 +117,7 @@ class TestComponentLogbook(unittest.TestCase): eventA = self.create_state_changed_event( pointA, entity_id, 10, attributes) - entries = list(logbook.humanify((eventA,))) + entries = list(logbook.humanify(self.hass, (eventA,))) self.assertEqual(0, len(entries)) @@ -133,7 +134,7 @@ class TestComponentLogbook(unittest.TestCase): events = logbook._exclude_events((ha.Event(EVENT_HOMEASSISTANT_STOP), eventA, eventB), {}) - entries = list(logbook.humanify(events)) + entries = list(logbook.humanify(self.hass, events)) self.assertEqual(2, len(entries)) self.assert_entry( @@ -155,7 +156,7 @@ class TestComponentLogbook(unittest.TestCase): events = logbook._exclude_events((ha.Event(EVENT_HOMEASSISTANT_STOP), eventA, eventB), {}) - entries = list(logbook.humanify(events)) + entries = list(logbook.humanify(self.hass, events)) self.assertEqual(2, len(entries)) self.assert_entry( @@ -177,7 +178,7 @@ class TestComponentLogbook(unittest.TestCase): events = logbook._exclude_events((ha.Event(EVENT_HOMEASSISTANT_STOP), eventA, eventB), {}) - entries = list(logbook.humanify(events)) + entries = list(logbook.humanify(self.hass, events)) self.assertEqual(2, len(entries)) self.assert_entry( @@ -203,7 +204,7 @@ class TestComponentLogbook(unittest.TestCase): events = logbook._exclude_events( (ha.Event(EVENT_HOMEASSISTANT_STOP), eventA, eventB), config[logbook.DOMAIN]) - entries = list(logbook.humanify(events)) + entries = list(logbook.humanify(self.hass, events)) self.assertEqual(2, len(entries)) self.assert_entry( @@ -229,7 +230,7 @@ class TestComponentLogbook(unittest.TestCase): events = logbook._exclude_events( (ha.Event(EVENT_HOMEASSISTANT_START), eventA, eventB), config[logbook.DOMAIN]) - entries = list(logbook.humanify(events)) + entries = list(logbook.humanify(self.hass, events)) self.assertEqual(2, len(entries)) self.assert_entry(entries[0], name='Home Assistant', message='started', @@ -266,7 +267,7 @@ class TestComponentLogbook(unittest.TestCase): events = logbook._exclude_events( (ha.Event(EVENT_HOMEASSISTANT_STOP), eventA, eventB), config[logbook.DOMAIN]) - entries = list(logbook.humanify(events)) + entries = list(logbook.humanify(self.hass, events)) self.assertEqual(2, len(entries)) self.assert_entry( @@ -292,7 +293,7 @@ class TestComponentLogbook(unittest.TestCase): events = logbook._exclude_events( (ha.Event(EVENT_HOMEASSISTANT_STOP), eventA, eventB), config[logbook.DOMAIN]) - entries = list(logbook.humanify(events)) + entries = list(logbook.humanify(self.hass, events)) self.assertEqual(2, len(entries)) self.assert_entry( @@ -318,7 +319,7 @@ class TestComponentLogbook(unittest.TestCase): events = logbook._exclude_events( (ha.Event(EVENT_HOMEASSISTANT_START), eventA, eventB), config[logbook.DOMAIN]) - entries = list(logbook.humanify(events)) + entries = list(logbook.humanify(self.hass, events)) self.assertEqual(2, len(entries)) self.assert_entry(entries[0], name='Home Assistant', message='started', @@ -352,7 +353,7 @@ class TestComponentLogbook(unittest.TestCase): events = logbook._exclude_events( (ha.Event(EVENT_HOMEASSISTANT_START), eventA1, eventA2, eventA3, eventB1, eventB2), config[logbook.DOMAIN]) - entries = list(logbook.humanify(events)) + entries = list(logbook.humanify(self.hass, events)) self.assertEqual(3, len(entries)) self.assert_entry(entries[0], name='Home Assistant', message='started', @@ -373,7 +374,7 @@ class TestComponentLogbook(unittest.TestCase): {'auto': True}) events = logbook._exclude_events((eventA, eventB), {}) - entries = list(logbook.humanify(events)) + entries = list(logbook.humanify(self.hass, events)) self.assertEqual(1, len(entries)) self.assert_entry(entries[0], pointA, 'bla', domain='switch', @@ -391,29 +392,18 @@ class TestComponentLogbook(unittest.TestCase): pointA, entity_id2, 20, last_changed=pointA, last_updated=pointB) events = logbook._exclude_events((eventA, eventB), {}) - entries = list(logbook.humanify(events)) + entries = list(logbook.humanify(self.hass, events)) self.assertEqual(1, len(entries)) self.assert_entry(entries[0], pointA, 'bla', domain='switch', entity_id=entity_id) - def test_entry_to_dict(self): - """Test conversion of entry to dict.""" - entry = logbook.Entry( - dt_util.utcnow(), 'Alarm', 'is triggered', 'switch', 'test_switch' - ) - data = entry.as_dict() - self.assertEqual('Alarm', data.get(logbook.ATTR_NAME)) - self.assertEqual('is triggered', data.get(logbook.ATTR_MESSAGE)) - self.assertEqual('switch', data.get(logbook.ATTR_DOMAIN)) - self.assertEqual('test_switch', data.get(logbook.ATTR_ENTITY_ID)) - def test_home_assistant_start_stop_grouped(self): """Test if HA start and stop events are grouped. Events that are occurring in the same minute. """ - entries = list(logbook.humanify(( + entries = list(logbook.humanify(self.hass, ( ha.Event(EVENT_HOMEASSISTANT_STOP), ha.Event(EVENT_HOMEASSISTANT_START), ))) @@ -428,7 +418,7 @@ class TestComponentLogbook(unittest.TestCase): entity_id = 'switch.bla' pointA = dt_util.utcnow() - entries = list(logbook.humanify(( + entries = list(logbook.humanify(self.hass, ( ha.Event(EVENT_HOMEASSISTANT_START), self.create_state_changed_event(pointA, entity_id, 10) ))) @@ -509,7 +499,7 @@ class TestComponentLogbook(unittest.TestCase): message = 'has a custom entry' entity_id = 'sun.sun' - entries = list(logbook.humanify(( + entries = list(logbook.humanify(self.hass, ( ha.Event(logbook.EVENT_LOGBOOK_ENTRY, { logbook.ATTR_NAME: name, logbook.ATTR_MESSAGE: message, @@ -526,19 +516,19 @@ class TestComponentLogbook(unittest.TestCase): domain=None, entity_id=None): """Assert an entry is what is expected.""" if when: - self.assertEqual(when, entry.when) + self.assertEqual(when, entry['when']) if name: - self.assertEqual(name, entry.name) + self.assertEqual(name, entry['name']) if message: - self.assertEqual(message, entry.message) + self.assertEqual(message, entry['message']) if domain: - self.assertEqual(domain, entry.domain) + self.assertEqual(domain, entry['domain']) if entity_id: - self.assertEqual(entity_id, entry.entity_id) + self.assertEqual(entity_id, entry['entity_id']) def create_state_changed_event(self, event_time_fired, entity_id, state, attributes=None, last_changed=None, @@ -566,3 +556,44 @@ async def test_logbook_view(hass, aiohttp_client): response = await client.get( '/api/logbook/{}'.format(dt_util.utcnow().isoformat())) assert response.status == 200 + + +async def test_humanify_alexa_event(hass): + """Test humanifying Alexa event.""" + hass.states.async_set('light.kitchen', 'on', { + 'friendly_name': 'Kitchen Light' + }) + + results = list(logbook.humanify(hass, [ + ha.Event(EVENT_ALEXA_SMART_HOME, { + 'namespace': 'Alexa.Discovery', + 'name': 'Discover', + }), + ha.Event(EVENT_ALEXA_SMART_HOME, { + 'namespace': 'Alexa.PowerController', + 'name': 'TurnOn', + 'entity_id': 'light.kitchen' + }), + ha.Event(EVENT_ALEXA_SMART_HOME, { + 'namespace': 'Alexa.PowerController', + 'name': 'TurnOn', + 'entity_id': 'light.non_existing' + }), + + ])) + + event1, event2, event3 = results + + assert event1['name'] == 'Amazon Alexa' + assert event1['message'] == 'send command Alexa.Discovery/Discover' + assert event1['entity_id'] is None + + assert event2['name'] == 'Amazon Alexa' + assert event2['message'] == \ + 'send command Alexa.PowerController/TurnOn for Kitchen Light' + assert event2['entity_id'] == 'light.kitchen' + + assert event3['name'] == 'Amazon Alexa' + assert event3['message'] == \ + 'send command Alexa.PowerController/TurnOn for light.non_existing' + assert event3['entity_id'] == 'light.non_existing' From 4c36ffd0ef0d0f9a8fce1a8522c5832e88ecaa31 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Mon, 1 Oct 2018 17:58:04 +0200 Subject: [PATCH 139/247] Remove error logging when Sonos shuffle_set is not available (#16921) Error on set_shuffle with UPnP Error 712 received: Play mode not supported from 10.23.2.16 --- homeassistant/components/media_player/sonos.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/media_player/sonos.py b/homeassistant/components/media_player/sonos.py index 07567590bdc..718db6300d0 100644 --- a/homeassistant/components/media_player/sonos.py +++ b/homeassistant/components/media_player/sonos.py @@ -69,7 +69,7 @@ ATTR_SPEECH_ENHANCE = 'speech_enhance' ATTR_SONOS_GROUP = 'sonos_group' -UPNP_ERRORS_TO_IGNORE = ['701', '711'] +UPNP_ERRORS_TO_IGNORE = ['701', '711', '712'] PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_ADVERTISE_ADDR): cv.string, @@ -831,7 +831,7 @@ class SonosDevice(MediaPlayerDevice): """Set volume level, range 0..1.""" self.soco.volume = str(int(volume * 100)) - @soco_error() + @soco_error(UPNP_ERRORS_TO_IGNORE) @soco_coordinator def set_shuffle(self, shuffle): """Enable/Disable shuffle mode.""" From b0c1c37cd5d204b3d457d25a22a28fbcf06b6a85 Mon Sep 17 00:00:00 2001 From: Nikolay Vasilchuk Date: Mon, 1 Oct 2018 19:00:48 +0300 Subject: [PATCH 140/247] Fix long update 'load_power' and 'in_use' for Xiaomi Zegbee Plug (#16915) --- homeassistant/components/switch/xiaomi_aqara.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/homeassistant/components/switch/xiaomi_aqara.py b/homeassistant/components/switch/xiaomi_aqara.py index a29a3c74a2e..17265d5dfa2 100644 --- a/homeassistant/components/switch/xiaomi_aqara.py +++ b/homeassistant/components/switch/xiaomi_aqara.py @@ -99,6 +99,11 @@ class XiaomiGenericSwitch(XiaomiDevice, SwitchDevice): attrs.update(super().device_state_attributes) return attrs + @property + def should_poll(self): + """Return the polling state. Polling needed for zigbee plug only.""" + return self._supports_power_consumption + def turn_on(self, **kwargs): """Turn the switch on.""" if self._write_to_hub(self._sid, **{self._data_key: 'on'}): @@ -131,3 +136,8 @@ class XiaomiGenericSwitch(XiaomiDevice, SwitchDevice): return False self._state = state return True + + def update(self): + """Get data from hub.""" + _LOGGER.debug("Update data from hub: %s", self._name) + self._get_from_hub(self._sid) From d732f8eca2201e0ad8ade2abb282f3507647de1e Mon Sep 17 00:00:00 2001 From: Steven Looman Date: Mon, 1 Oct 2018 18:25:54 +0200 Subject: [PATCH 141/247] Changes after review by @MartinHjelmare --- homeassistant/components/sensor/upnp.py | 90 ++++++++++++------- .../components/upnp/.translations/en.json | 6 +- .../components/upnp/.translations/nl.json | 6 +- homeassistant/components/upnp/__init__.py | 31 +++---- homeassistant/components/upnp/config_flow.py | 34 +++---- homeassistant/components/upnp/device.py | 3 +- homeassistant/components/upnp/strings.json | 6 +- tests/components/upnp/test_config_flow.py | 58 ++++++------ tests/components/upnp/test_init.py | 28 +++--- 9 files changed, 147 insertions(+), 115 deletions(-) diff --git a/homeassistant/components/sensor/upnp.py b/homeassistant/components/sensor/upnp.py index 3c3745145a4..e511b2947e5 100644 --- a/homeassistant/components/sensor/upnp.py +++ b/homeassistant/components/sensor/upnp.py @@ -8,8 +8,10 @@ https://home-assistant.io/components/sensor.upnp/ from datetime import datetime import logging -from homeassistant.components.upnp import DOMAIN +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity +from homeassistant.components.upnp.const import DOMAIN as DATA_UPNP _LOGGER = logging.getLogger(__name__) @@ -45,37 +47,65 @@ OUT = 'sent' KBYTE = 1024 -async def async_setup_platform(hass, config, async_add_devices, +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the UPnP/IGD sensors.""" - if discovery_info is None: - return - - udn = discovery_info['udn'] - device = hass.data[DOMAIN]['devices'][udn] - - # raw sensors + per-second sensors - sensors = [ - RawUPnPIGDSensor(device, name, sensor_type) - for name, sensor_type in SENSOR_TYPES.items() - ] - sensors += [ - KBytePerSecondUPnPIGDSensor(device, IN), - KBytePerSecondUPnPIGDSensor(device, OUT), - PacketsPerSecondUPnPIGDSensor(device, IN), - PacketsPerSecondUPnPIGDSensor(device, OUT), - ] - hass.data[DOMAIN]['sensors'][udn] = sensors - async_add_devices(sensors, True) - return True + """Old way of setting up UPnP/IGD sensors.""" + _LOGGER.debug('async_setup_platform: config: %s, discovery: %s', + config, discovery_info) -class RawUPnPIGDSensor(Entity): +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the UPnP/IGD sensor.""" + @callback + def async_add_sensor(device): + """Add sensors from UPnP/IGD device.""" + # raw sensors + per-second sensors + sensors = [ + RawUPnPIGDSensor(device, name, sensor_type) + for name, sensor_type in SENSOR_TYPES.items() + ] + sensors += [ + KBytePerSecondUPnPIGDSensor(device, IN), + KBytePerSecondUPnPIGDSensor(device, OUT), + PacketsPerSecondUPnPIGDSensor(device, IN), + PacketsPerSecondUPnPIGDSensor(device, OUT), + ] + async_add_entities(sensors, True) + + data = config_entry.data + udn = data['udn'] + device = hass.data[DATA_UPNP]['devices'][udn] + async_add_sensor(device) + + +class UpnpSensor(Entity): + """Base class for UPnP/IGD sensors.""" + + def __init__(self, device): + """Initialize the base sensor.""" + self._device = device + + async def async_added_to_hass(self): + """Subscribe to sensors events.""" + async_dispatcher_connect(self.hass, + 'upnp_remove_sensor', + self._upnp_remove_sensor) + + def _upnp_remove_sensor(self, device): + """Remove sensor.""" + if self._device != device: + # not for us + return + + self.hass.async_create_task(self.async_remove()) + + +class RawUPnPIGDSensor(UpnpSensor): """Representation of a UPnP/IGD sensor.""" def __init__(self, device, sensor_type_name, sensor_type): """Initialize the UPnP/IGD sensor.""" - self._device = device + super().__init__(device) self._type_name = sensor_type_name self._type = sensor_type self._name = '{} {}'.format(device.name, sensor_type['name']) @@ -94,7 +124,7 @@ class RawUPnPIGDSensor(Entity): @property def state(self) -> str: """Return the state of the device.""" - return self._state + return format(self._state, 'd') @property def icon(self) -> str: @@ -118,12 +148,12 @@ class RawUPnPIGDSensor(Entity): self._state = await self._device.async_get_total_packets_sent() -class PerSecondUPnPIGDSensor(Entity): +class PerSecondUPnPIGDSensor(UpnpSensor): """Abstract representation of a X Sent/Received per second sensor.""" def __init__(self, device, direction): """Initializer.""" - self._device = device + super().__init__(device) self._direction = direction self._state = None @@ -205,7 +235,7 @@ class KBytePerSecondUPnPIGDSensor(PerSecondUPnPIGDSensor): return await self._device.async_get_total_bytes_sent() @property - def state(self): + def state(self) -> str: """Return the state of the device.""" if self._state is None: return None @@ -229,7 +259,7 @@ class PacketsPerSecondUPnPIGDSensor(PerSecondUPnPIGDSensor): return await self._device.async_get_total_packets_sent() @property - def state(self): + def state(self) -> str: """Return the state of the device.""" if self._state is None: return None diff --git a/homeassistant/components/upnp/.translations/en.json b/homeassistant/components/upnp/.translations/en.json index 829631254ad..682150b7ddd 100644 --- a/homeassistant/components/upnp/.translations/en.json +++ b/homeassistant/components/upnp/.translations/en.json @@ -9,8 +9,8 @@ "title": "Configuration options for the UPnP/IGD", "data":{ "igd": "UPnP/IGD", - "sensors": "Add traffic sensors", - "port_forward": "Enable port forward for Home Assistant" + "enable_sensors": "Add traffic sensors", + "enable_port_mapping": "Enable port mapping for Home Assistant" } } }, @@ -19,7 +19,7 @@ "abort": { "no_devices_discovered": "No UPnP/IGDs discovered", "already_configured": "UPnP/IGD is already configured", - "no_sensors_or_port_forward": "Enable at least sensors or port forward" + "no_sensors_or_port_mapping": "Enable at least sensors or port mapping" } } } diff --git a/homeassistant/components/upnp/.translations/nl.json b/homeassistant/components/upnp/.translations/nl.json index 02d8fcc0913..55a94a8ea6f 100644 --- a/homeassistant/components/upnp/.translations/nl.json +++ b/homeassistant/components/upnp/.translations/nl.json @@ -9,8 +9,8 @@ "title": "Extra configuratie options voor UPnP/IGD", "data":{ "igd": "UPnP/IGD", - "sensors": "Verkeer sensors toevoegen", - "port_forward": "Maak port forward voor Home Assistant" + "enable_sensors": "Verkeer sensors toevoegen", + "enable_port_mapping": "Maak port mapping voor Home Assistant" } } }, @@ -19,7 +19,7 @@ "abort": { "no_devices_discovered": "Geen UPnP/IGDs gevonden", "already_configured": "UPnP/IGD is reeds geconfigureerd", - "no_sensors_or_port_forward": "Kies ten minste sensors of port forward" + "no_sensors_or_port_mapping": "Kies ten minste sensors of port mapping" } } } diff --git a/homeassistant/components/upnp/__init__.py b/homeassistant/components/upnp/__init__.py index 25650e6e637..4ccb07af44b 100644 --- a/homeassistant/components/upnp/__init__.py +++ b/homeassistant/components/upnp/__init__.py @@ -4,6 +4,7 @@ Will open a port in your router for Home Assistant and provide statistics. For more details about this component, please refer to the documentation at https://home-assistant.io/components/upnp/ """ +# pylint: disable=invalid-name import asyncio from ipaddress import ip_address @@ -12,9 +13,8 @@ import voluptuous as vol from homeassistant.config_entries import ConfigEntry from homeassistant.const import EVENT_HOMEASSISTANT_STOP -from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers import config_validation as cv -from homeassistant.helpers import discovery +from homeassistant.helpers import dispatcher from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import HomeAssistantType from homeassistant.components.discovery import DOMAIN as DISCOVERY_DOMAIN @@ -79,16 +79,16 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType): hass.data[DOMAIN]['local_ip'] = upnp_config[CONF_LOCAL_IP] # determine ports - ports = {CONF_HASS: CONF_HASS} # default, port_forward disabled by default + ports = {CONF_HASS: CONF_HASS} # default, port_mapping disabled by default if CONF_PORTS in upnp_config: # copy from config ports = upnp_config[CONF_PORTS] hass.data[DOMAIN]['auto_config'] = { 'active': True, - 'port_forward': upnp_config[CONF_ENABLE_PORT_MAPPING], + 'enable_sensors': upnp_config[CONF_ENABLE_SENSORS], + 'enable_port_mapping': upnp_config[CONF_ENABLE_PORT_MAPPING], 'ports': ports, - 'sensors': upnp_config[CONF_ENABLE_SENSORS], } return True @@ -98,7 +98,6 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType): async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigEntry): """Set up a bridge from a config entry.""" - _LOGGER.debug('async_setup_entry: %s', config_entry.data) ensure_domain_data(hass) data = config_entry.data @@ -107,7 +106,8 @@ async def async_setup_entry(hass: HomeAssistantType, try: device = await Device.async_create_device(hass, ssdp_description) except (asyncio.TimeoutError, aiohttp.ClientError): - raise PlatformNotReady() + _LOGGER.error('Unable to create upnp-device') + return hass.data[DOMAIN]['devices'][device.udn] = device @@ -124,12 +124,10 @@ async def async_setup_entry(hass: HomeAssistantType, # sensors if data.get(CONF_ENABLE_SENSORS): _LOGGER.debug('Enabling sensors') - discovery_info = { - 'udn': device.udn, - } - hass_config = config_entry.data - hass.async_create_task(discovery.async_load_platform( - hass, 'sensor', DOMAIN, discovery_info, hass_config)) + + # register sensor setup handlers + hass.async_create_task(hass.config_entries.async_forward_entry_setup( + config_entry, 'sensor')) async def unload_entry(event): """Unload entry on quit.""" @@ -142,7 +140,6 @@ async def async_setup_entry(hass: HomeAssistantType, async def async_unload_entry(hass: HomeAssistantType, config_entry: ConfigEntry): """Unload a config entry.""" - _LOGGER.debug('async_unload_entry: %s', config_entry.data) data = config_entry.data udn = data[CONF_UDN] @@ -156,9 +153,9 @@ async def async_unload_entry(hass: HomeAssistantType, await device.async_delete_port_mappings() # sensors - for sensor in hass.data[DOMAIN]['sensors'].get(udn, []): - _LOGGER.debug('Deleting sensor: %s', sensor) - await sensor.async_remove() + if data.get(CONF_ENABLE_SENSORS): + _LOGGER.debug('Deleting sensors') + dispatcher.async_dispatcher_send(hass, 'upnp_remove_sensor', device) # clear stored device del hass.data[DOMAIN]['devices'][udn] diff --git a/homeassistant/components/upnp/config_flow.py b/homeassistant/components/upnp/config_flow.py index 95deed73e63..65e2283115c 100644 --- a/homeassistant/components/upnp/config_flow.py +++ b/homeassistant/components/upnp/config_flow.py @@ -1,4 +1,6 @@ """Config flow for UPNP.""" +from collections import OrderedDict + import voluptuous as vol from homeassistant import config_entries @@ -15,12 +17,11 @@ def ensure_domain_data(hass): """Ensure hass.data is filled properly.""" hass.data[DOMAIN] = hass.data.get(DOMAIN, {}) hass.data[DOMAIN]['devices'] = hass.data[DOMAIN].get('devices', {}) - hass.data[DOMAIN]['sensors'] = hass.data[DOMAIN].get('sensors', {}) hass.data[DOMAIN]['discovered'] = hass.data[DOMAIN].get('discovered', {}) hass.data[DOMAIN]['auto_config'] = hass.data[DOMAIN].get('auto_config', { 'active': False, - 'port_forward': False, - 'sensors': False, + 'enable_sensors': False, + 'enable_port_mapping': False, 'ports': {'hass': 'hass'}, }) @@ -30,6 +31,7 @@ class UpnpFlowHandler(data_entry_flow.FlowHandler): """Handle a Hue config flow.""" VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL @property def _configured_upnp_igds(self): @@ -79,8 +81,8 @@ class UpnpFlowHandler(data_entry_flow.FlowHandler): if auto_config['active']: import_info = { 'name': discovery_info['friendly_name'], - 'sensors': auto_config['sensors'], - 'port_forward': auto_config['port_forward'], + 'enable_sensors': auto_config['enable_sensors'], + 'enable_port_mapping': auto_config['enable_port_mapping'], } return await self._async_save_entry(import_info) @@ -94,8 +96,9 @@ class UpnpFlowHandler(data_entry_flow.FlowHandler): # if user input given, handle it user_input = user_input or {} if 'name' in user_input: - if not user_input['sensors'] and not user_input['port_forward']: - return self.async_abort(reason='no_sensors_or_port_forward') + if not user_input['enable_sensors'] and \ + not user_input['enable_port_mapping']: + return self.async_abort(reason='no_sensors_or_port_mapping') # ensure not already configured configured_names = [ @@ -119,12 +122,13 @@ class UpnpFlowHandler(data_entry_flow.FlowHandler): return self.async_show_form( step_id='user', - data_schema=vol.Schema({ - vol.Required('name'): vol.In(names), - vol.Optional('sensors', default=False): bool, - vol.Optional('port_forward', default=False): bool, - }) - ) + data_schema=vol.Schema( + OrderedDict([ + (vol.Required('name'), vol.In(names)), + (vol.Optional('enable_sensors', default=False), bool), + (vol.Optional('enable_port_mapping', default=False), bool), + ]) + )) async def async_step_import(self, import_info): """Import a new UPnP/IGD as a config entry.""" @@ -150,7 +154,7 @@ class UpnpFlowHandler(data_entry_flow.FlowHandler): data={ CONF_SSDP_DESCRIPTION: discovery_info['ssdp_description'], CONF_UDN: discovery_info['udn'], - CONF_ENABLE_SENSORS: import_info['sensors'], - CONF_ENABLE_PORT_MAPPING: import_info['port_forward'], + CONF_ENABLE_SENSORS: import_info['enable_sensors'], + CONF_ENABLE_PORT_MAPPING: import_info['enable_port_mapping'], }, ) diff --git a/homeassistant/components/upnp/device.py b/homeassistant/components/upnp/device.py index 6dce3889eaf..4a444aa3087 100644 --- a/homeassistant/components/upnp/device.py +++ b/homeassistant/components/upnp/device.py @@ -39,7 +39,7 @@ class Device: from async_upnp_client.igd import IgdDevice igd_device = IgdDevice(upnp_device, None) - return Device(igd_device) + return cls(igd_device) @property def udn(self): @@ -102,6 +102,7 @@ class Device: async def _async_delete_port_mapping(self, external_port): """Remove a port mapping.""" from async_upnp_client import UpnpError + _LOGGER.info('Deleting port mapping %s (TCP)', external_port) try: await self._igd_device.async_delete_port_mapping( remote_host=None, diff --git a/homeassistant/components/upnp/strings.json b/homeassistant/components/upnp/strings.json index 829631254ad..682150b7ddd 100644 --- a/homeassistant/components/upnp/strings.json +++ b/homeassistant/components/upnp/strings.json @@ -9,8 +9,8 @@ "title": "Configuration options for the UPnP/IGD", "data":{ "igd": "UPnP/IGD", - "sensors": "Add traffic sensors", - "port_forward": "Enable port forward for Home Assistant" + "enable_sensors": "Add traffic sensors", + "enable_port_mapping": "Enable port mapping for Home Assistant" } } }, @@ -19,7 +19,7 @@ "abort": { "no_devices_discovered": "No UPnP/IGDs discovered", "already_configured": "UPnP/IGD is already configured", - "no_sensors_or_port_forward": "Enable at least sensors or port forward" + "no_sensors_or_port_mapping": "Enable at least sensors or port mapping" } } } diff --git a/tests/components/upnp/test_config_flow.py b/tests/components/upnp/test_config_flow.py index f6ec05a42ab..3ff1316975f 100644 --- a/tests/components/upnp/test_config_flow.py +++ b/tests/components/upnp/test_config_flow.py @@ -44,15 +44,15 @@ async def test_flow_already_configured(hass): result = await flow.async_step_user({ 'name': '192.168.1.1 (Test device)', - 'sensors': True, - 'port_forward': False, + 'enable_sensors': True, + 'enable_port_mapping': False, }) assert result['type'] == 'abort' assert result['reason'] == 'already_configured' -async def test_flow_no_sensors_no_port_forward(hass): - """Test single device, no sensors, no port_forward.""" +async def test_flow_no_sensors_no_port_mapping(hass): + """Test single device, no sensors, no port_mapping.""" flow = upnp_config_flow.UpnpFlowHandler() flow.hass = hass @@ -76,11 +76,11 @@ async def test_flow_no_sensors_no_port_forward(hass): result = await flow.async_step_user({ 'name': '192.168.1.1 (Test device)', - 'sensors': False, - 'port_forward': False, + 'enable_sensors': False, + 'enable_port_mapping': False, }) assert result['type'] == 'abort' - assert result['reason'] == 'no_sensors_or_port_forward' + assert result['reason'] == 'no_sensors_or_port_mapping' async def test_flow_discovered_form(hass): @@ -106,7 +106,7 @@ async def test_flow_discovered_form(hass): async def test_flow_two_discovered_form(hass): - """Test single device discovered, show form flow.""" + """Test two devices discovered, show form flow with two devices.""" flow = upnp_config_flow.UpnpFlowHandler() flow.hass = hass @@ -133,13 +133,13 @@ async def test_flow_two_discovered_form(hass): assert result['step_id'] == 'user' assert result['data_schema']({ 'name': '192.168.1.1 (Test device)', - 'sensors': True, - 'port_forward': False, + 'enable_sensors': True, + 'enable_port_mapping': False, }) assert result['data_schema']({ - 'name': '192.168.1.1 (Test device)', - 'sensors': True, - 'port_forward': False, + 'name': '192.168.2.1 (Test device)', + 'enable_sensors': True, + 'enable_port_mapping': False, }) @@ -163,15 +163,15 @@ async def test_config_entry_created(hass): result = await flow.async_step_user({ 'name': '192.168.1.1 (Test device)', - 'sensors': True, - 'port_forward': False, + 'enable_sensors': True, + 'enable_port_mapping': False, }) assert result['type'] == 'create_entry' assert result['data'] == { - 'port_forward': False, - 'sensors': True, 'ssdp_description': 'http://192.168.1.1/desc.xml', 'udn': 'uuid:device_1', + 'port_mapping': False, + 'sensors': True, } assert result['title'] == 'Test device 1' @@ -185,8 +185,8 @@ async def test_flow_discovery_auto_config_sensors(hass): hass.data[upnp.DOMAIN] = { 'auto_config': { 'active': True, - 'port_forward': False, - 'sensors': True, + 'enable_port_mapping': False, + 'enable_sensors': True, }, } @@ -200,25 +200,25 @@ async def test_flow_discovery_auto_config_sensors(hass): assert result['type'] == 'create_entry' assert result['data'] == { - 'port_forward': False, - 'sensors': True, 'ssdp_description': 'http://192.168.1.1/desc.xml', 'udn': 'uuid:device_1', + 'sensors': True, + 'port_mapping': False, } assert result['title'] == 'Test device 1' -async def test_flow_discovery_auto_config_sensors_port_forward(hass): - """Test creation of device with auto_config, with port forward.""" +async def test_flow_discovery_auto_config_sensors_port_mapping(hass): + """Test creation of device with auto_config, with port mapping.""" flow = upnp_config_flow.UpnpFlowHandler() flow.hass = hass - # auto_config active, with port_forward + # auto_config active, with port_mapping hass.data[upnp.DOMAIN] = { 'auto_config': { 'active': True, - 'port_forward': True, - 'sensors': True, + 'enable_port_mapping': True, + 'enable_sensors': True, }, } @@ -232,9 +232,9 @@ async def test_flow_discovery_auto_config_sensors_port_forward(hass): assert result['type'] == 'create_entry' assert result['data'] == { - 'port_forward': True, - 'sensors': True, - 'ssdp_description': 'http://192.168.1.1/desc.xml', 'udn': 'uuid:device_1', + 'ssdp_description': 'http://192.168.1.1/desc.xml', + 'sensors': True, + 'port_mapping': True, } assert result['title'] == 'Test device 1' diff --git a/tests/components/upnp/test_init.py b/tests/components/upnp/test_init.py index 581abc3190c..ce4656032a6 100644 --- a/tests/components/upnp/test_init.py +++ b/tests/components/upnp/test_init.py @@ -53,9 +53,9 @@ async def test_async_setup_no_auto_config(hass): assert hass.data[upnp.DOMAIN]['auto_config'] == { 'active': False, - 'port_forward': False, + 'enable_sensors': False, + 'enable_port_mapping': False, 'ports': {'hass': 'hass'}, - 'sensors': False, } @@ -66,27 +66,27 @@ async def test_async_setup_auto_config(hass): assert hass.data[upnp.DOMAIN]['auto_config'] == { 'active': True, - 'port_forward': False, + 'enable_sensors': True, + 'enable_port_mapping': False, 'ports': {'hass': 'hass'}, - 'sensors': True, } -async def test_async_setup_auto_config_port_forward(hass): +async def test_async_setup_auto_config_port_mapping(hass): """Test async_setup.""" # setup component, enable auto_config await async_setup_component(hass, 'upnp', { 'upnp': { - 'port_forward': True, + 'port_mapping': True, 'ports': {'hass': 'hass'}, }, 'discovery': {}}) assert hass.data[upnp.DOMAIN]['auto_config'] == { 'active': True, - 'port_forward': True, + 'enable_sensors': True, + 'enable_port_mapping': True, 'ports': {'hass': 'hass'}, - 'sensors': True, } @@ -99,9 +99,9 @@ async def test_async_setup_auto_config_no_sensors(hass): assert hass.data[upnp.DOMAIN]['auto_config'] == { 'active': True, - 'port_forward': False, + 'enable_sensors': False, + 'enable_port_mapping': False, 'ports': {'hass': 'hass'}, - 'sensors': False, } @@ -112,7 +112,7 @@ async def test_async_setup_entry_default(hass): 'ssdp_description': 'http://192.168.1.1/desc.xml', 'udn': udn, 'sensors': True, - 'port_forward': False, + 'port_mapping': False, }) # ensure hass.http is available @@ -144,20 +144,20 @@ async def test_async_setup_entry_default(hass): assert len(mock_device.async_delete_port_mappings.mock_calls) == 0 -async def test_async_setup_entry_port_forward(hass): +async def test_async_setup_entry_port_mapping(hass): """Test async_setup_entry.""" udn = 'uuid:device_1' entry = MockConfigEntry(domain=upnp.DOMAIN, data={ 'ssdp_description': 'http://192.168.1.1/desc.xml', 'udn': udn, 'sensors': False, - 'port_forward': True, + 'port_mapping': True, }) # ensure hass.http is available await async_setup_component(hass, 'upnp', { 'upnp': { - 'port_forward': True, + 'port_mapping': True, 'ports': {'hass': 'hass'}, }, 'discovery': {}, From a148f3e2a972132d5c67c545ffa263962e68e747 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Mon, 1 Oct 2018 20:38:41 +0200 Subject: [PATCH 142/247] Mind the unit system (fixes #16819) (#16823) --- homeassistant/components/weather/openweathermap.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/weather/openweathermap.py b/homeassistant/components/weather/openweathermap.py index 87d2bc6683f..b70413b9565 100644 --- a/homeassistant/components/weather/openweathermap.py +++ b/homeassistant/components/weather/openweathermap.py @@ -130,6 +130,9 @@ class OpenWeatherMapWeather(WeatherEntity): @property def wind_speed(self): """Return the wind speed.""" + if self.hass.config.units.name == 'imperial': + return round(self.data.get_wind().get('speed') * 2.24, 2) + return round(self.data.get_wind().get('speed') * 3.6, 2) @property From b197b8bab34008ce91d6b7bb1e3c4ebb1589d938 Mon Sep 17 00:00:00 2001 From: Charles Garwood Date: Mon, 1 Oct 2018 14:39:40 -0400 Subject: [PATCH 143/247] Suppress urllib3 header parsing error (#17042) --- homeassistant/components/camera/mjpeg.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/homeassistant/components/camera/mjpeg.py b/homeassistant/components/camera/mjpeg.py index b51ec982ccd..f758eb11e9d 100644 --- a/homeassistant/components/camera/mjpeg.py +++ b/homeassistant/components/camera/mjpeg.py @@ -74,6 +74,10 @@ class MjpegCamera(Camera): self._mjpeg_url = device_info[CONF_MJPEG_URL] self._still_image_url = device_info.get(CONF_STILL_IMAGE_URL) + logging.getLogger("urllib3.connectionpool").addFilter( + NoHeaderErrorFilter() + ) + self._auth = None if self._username and self._password: if self._authentication == HTTP_BASIC_AUTHENTICATION: @@ -139,3 +143,11 @@ class MjpegCamera(Camera): def name(self): """Return the name of this camera.""" return self._name + + +class NoHeaderErrorFilter(logging.Filter): + """Filter out urllib3 Header Parsing Errors due to a urllib3 bug.""" + + def filter(self, record): + """Filter out Header Parsing Errors.""" + return "Failed to parse headers" not in record.getMessage() From 90f2990b9e0dbd6251c927106508ae5aa8161656 Mon Sep 17 00:00:00 2001 From: SNoof85 Date: Mon, 1 Oct 2018 22:18:56 +0200 Subject: [PATCH 144/247] Support code 7 (#17047) --- homeassistant/components/vacuum/xiaomi_miio.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/vacuum/xiaomi_miio.py b/homeassistant/components/vacuum/xiaomi_miio.py index 78b3b4434d6..d2da4f3b6ac 100644 --- a/homeassistant/components/vacuum/xiaomi_miio.py +++ b/homeassistant/components/vacuum/xiaomi_miio.py @@ -92,6 +92,7 @@ STATE_CODE_TO_STATE = { 3: STATE_IDLE, 5: STATE_CLEANING, 6: STATE_RETURNING, + 7: STATE_CLEANING, 8: STATE_DOCKED, 9: STATE_ERROR, 10: STATE_PAUSED, From 4d471622f677d7f63f04492f22e1a6af0523b13f Mon Sep 17 00:00:00 2001 From: Daniel Perna Date: Tue, 2 Oct 2018 00:02:59 +0200 Subject: [PATCH 145/247] Update pyhomematic to 0.1.50 (#17048) --- homeassistant/components/homematic/__init__.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/homematic/__init__.py b/homeassistant/components/homematic/__init__.py index b2b3b18ff34..927f86b590d 100644 --- a/homeassistant/components/homematic/__init__.py +++ b/homeassistant/components/homematic/__init__.py @@ -18,7 +18,7 @@ from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['pyhomematic==0.1.49'] +REQUIREMENTS = ['pyhomematic==0.1.50'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 2fac42738de..b1d8f5dc4f3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -909,7 +909,7 @@ pyhik==0.1.8 pyhiveapi==0.2.14 # homeassistant.components.homematic -pyhomematic==0.1.49 +pyhomematic==0.1.50 # homeassistant.components.sensor.hydroquebec pyhydroquebec==2.2.2 From c3eff5773b4e6061c6091af326914b31175bbc3a Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Tue, 2 Oct 2018 00:33:45 +0200 Subject: [PATCH 146/247] Remove alert service helper (#17038) --- homeassistant/components/alert.py | 40 ---------------- tests/components/test_alert.py | 77 +++++++++++++++++++++++++++---- 2 files changed, 68 insertions(+), 49 deletions(-) diff --git a/homeassistant/components/alert.py b/homeassistant/components/alert.py index 68f471c33ab..72689b30138 100644 --- a/homeassistant/components/alert.py +++ b/homeassistant/components/alert.py @@ -10,7 +10,6 @@ import logging import voluptuous as vol -from homeassistant.core import callback from homeassistant.const import ( CONF_ENTITY_ID, STATE_IDLE, CONF_NAME, CONF_STATE, STATE_ON, STATE_OFF, SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE, ATTR_ENTITY_ID) @@ -59,45 +58,6 @@ def is_on(hass, entity_id): return hass.states.is_state(entity_id, STATE_ON) -def turn_on(hass, entity_id): - """Reset the alert.""" - hass.add_job(async_turn_on, hass, entity_id) - - -@callback -def async_turn_on(hass, entity_id): - """Async reset the alert.""" - data = {ATTR_ENTITY_ID: entity_id} - hass.async_create_task( - hass.services.async_call(DOMAIN, SERVICE_TURN_ON, data)) - - -def turn_off(hass, entity_id): - """Acknowledge alert.""" - hass.add_job(async_turn_off, hass, entity_id) - - -@callback -def async_turn_off(hass, entity_id): - """Async acknowledge the alert.""" - data = {ATTR_ENTITY_ID: entity_id} - hass.async_create_task( - hass.services.async_call(DOMAIN, SERVICE_TURN_OFF, data)) - - -def toggle(hass, entity_id): - """Toggle acknowledgement of alert.""" - hass.add_job(async_toggle, hass, entity_id) - - -@callback -def async_toggle(hass, entity_id): - """Async toggle acknowledgement of alert.""" - data = {ATTR_ENTITY_ID: entity_id} - hass.async_create_task( - hass.services.async_call(DOMAIN, SERVICE_TOGGLE, data)) - - async def async_setup(hass, config): """Set up the Alert component.""" alerts = config.get(DOMAIN) diff --git a/tests/components/test_alert.py b/tests/components/test_alert.py index 90d732ac38e..44ece2fc38e 100644 --- a/tests/components/test_alert.py +++ b/tests/components/test_alert.py @@ -5,10 +5,12 @@ import unittest from homeassistant.setup import setup_component from homeassistant.core import callback +from homeassistant.components.alert import DOMAIN import homeassistant.components.alert as alert import homeassistant.components.notify as notify -from homeassistant.const import (CONF_ENTITY_ID, STATE_IDLE, CONF_NAME, - CONF_STATE, STATE_ON, STATE_OFF) +from homeassistant.const import ( + ATTR_ENTITY_ID, CONF_ENTITY_ID, STATE_IDLE, CONF_NAME, CONF_STATE, + SERVICE_TOGGLE, SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_ON, STATE_OFF) from tests.common import get_test_home_assistant @@ -31,6 +33,63 @@ TEST_NOACK = [NAME, NAME, DONE_MESSAGE, "sensor.test", ENTITY_ID = alert.ENTITY_ID_FORMAT.format(NAME) +def turn_on(hass, entity_id): + """Reset the alert. + + This is a legacy helper method. Do not use it for new tests. + """ + hass.add_job(async_turn_on, hass, entity_id) + + +@callback +def async_turn_on(hass, entity_id): + """Async reset the alert. + + This is a legacy helper method. Do not use it for new tests. + """ + data = {ATTR_ENTITY_ID: entity_id} + hass.async_create_task( + hass.services.async_call(DOMAIN, SERVICE_TURN_ON, data)) + + +def turn_off(hass, entity_id): + """Acknowledge alert. + + This is a legacy helper method. Do not use it for new tests. + """ + hass.add_job(async_turn_off, hass, entity_id) + + +@callback +def async_turn_off(hass, entity_id): + """Async acknowledge the alert. + + This is a legacy helper method. Do not use it for new tests. + """ + data = {ATTR_ENTITY_ID: entity_id} + hass.async_create_task( + hass.services.async_call(DOMAIN, SERVICE_TURN_OFF, data)) + + +def toggle(hass, entity_id): + """Toggle acknowledgment of alert. + + This is a legacy helper method. Do not use it for new tests. + """ + hass.add_job(async_toggle, hass, entity_id) + + +@callback +def async_toggle(hass, entity_id): + """Async toggle acknowledgment of alert. + + This is a legacy helper method. Do not use it for new tests. + """ + data = {ATTR_ENTITY_ID: entity_id} + hass.async_create_task( + hass.services.async_call(DOMAIN, SERVICE_TOGGLE, data)) + + # pylint: disable=invalid-name class TestAlert(unittest.TestCase): """Test the alert module.""" @@ -69,7 +128,7 @@ class TestAlert(unittest.TestCase): assert setup_component(self.hass, alert.DOMAIN, TEST_CONFIG) self.hass.states.set("sensor.test", STATE_ON) self.hass.block_till_done() - alert.turn_off(self.hass, ENTITY_ID) + turn_off(self.hass, ENTITY_ID) self.hass.block_till_done() self.assertEqual(STATE_OFF, self.hass.states.get(ENTITY_ID).state) @@ -86,10 +145,10 @@ class TestAlert(unittest.TestCase): assert setup_component(self.hass, alert.DOMAIN, TEST_CONFIG) self.hass.states.set("sensor.test", STATE_ON) self.hass.block_till_done() - alert.turn_off(self.hass, ENTITY_ID) + turn_off(self.hass, ENTITY_ID) self.hass.block_till_done() self.assertEqual(STATE_OFF, self.hass.states.get(ENTITY_ID).state) - alert.turn_on(self.hass, ENTITY_ID) + turn_on(self.hass, ENTITY_ID) self.hass.block_till_done() self.assertEqual(STATE_ON, self.hass.states.get(ENTITY_ID).state) @@ -99,10 +158,10 @@ class TestAlert(unittest.TestCase): self.hass.states.set("sensor.test", STATE_ON) self.hass.block_till_done() self.assertEqual(STATE_ON, self.hass.states.get(ENTITY_ID).state) - alert.toggle(self.hass, ENTITY_ID) + toggle(self.hass, ENTITY_ID) self.hass.block_till_done() self.assertEqual(STATE_OFF, self.hass.states.get(ENTITY_ID).state) - alert.toggle(self.hass, ENTITY_ID) + toggle(self.hass, ENTITY_ID) self.hass.block_till_done() self.assertEqual(STATE_ON, self.hass.states.get(ENTITY_ID).state) @@ -117,7 +176,7 @@ class TestAlert(unittest.TestCase): hidden = self.hass.states.get(ENTITY_ID).attributes.get('hidden') self.assertFalse(hidden) - alert.turn_off(self.hass, ENTITY_ID) + turn_off(self.hass, ENTITY_ID) hidden = self.hass.states.get(ENTITY_ID).attributes.get('hidden') self.assertFalse(hidden) @@ -199,7 +258,7 @@ class TestAlert(unittest.TestCase): self.assertEqual(True, entity.hidden) def test_done_message_state_tracker_reset_on_cancel(self): - """Test that the done message is reset when cancelled.""" + """Test that the done message is reset when canceled.""" entity = alert.Alert(self.hass, *TEST_NOACK) entity._cancel = lambda *args: None assert entity._send_done_message is False From 284d4d49c7ded90e8dbe847e4c29d8e21717db61 Mon Sep 17 00:00:00 2001 From: Sean Wilson Date: Tue, 2 Oct 2018 01:32:03 -0400 Subject: [PATCH 147/247] Add AquaLogic component (#16763) * Add missing ups.status states. * Add missing DISCHRG state. * AquaLogic work-in-progress * - Fix dependencies - Switch updates * Add support for aqualogic 0.8 features. * Remove debugging. * Switch to async updates rather than using polling. * Rebase * Fix lint errors * Fix lint errors * Fix lint errors * Fix lint errors * Fix lint errors. * Bump aqualogic version to 0.11 * Update .coveragerc * Remove integration-specific I/O * Resolve code review issues. * Fixed init() call. --- .coveragerc | 3 + homeassistant/components/aqualogic.py | 95 ++++++++++++++++ homeassistant/components/sensor/aqualogic.py | 111 ++++++++++++++++++ homeassistant/components/switch/aqualogic.py | 114 +++++++++++++++++++ requirements_all.txt | 3 + 5 files changed, 326 insertions(+) create mode 100644 homeassistant/components/aqualogic.py create mode 100644 homeassistant/components/sensor/aqualogic.py create mode 100644 homeassistant/components/switch/aqualogic.py diff --git a/.coveragerc b/.coveragerc index e13e9fe0002..fae3ebebbe7 100644 --- a/.coveragerc +++ b/.coveragerc @@ -28,6 +28,9 @@ omit = homeassistant/components/apple_tv.py homeassistant/components/*/apple_tv.py + homeassistant/components/aqualogic.py + homeassistant/components/*/aqualogic.py + homeassistant/components/arduino.py homeassistant/components/*/arduino.py diff --git a/homeassistant/components/aqualogic.py b/homeassistant/components/aqualogic.py new file mode 100644 index 00000000000..abb61d42ca3 --- /dev/null +++ b/homeassistant/components/aqualogic.py @@ -0,0 +1,95 @@ +""" +Support for AquaLogic component. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/aqualogic/ +""" +from datetime import timedelta +import logging +import time +import threading + +import voluptuous as vol + +from homeassistant.const import (CONF_HOST, CONF_PORT, + EVENT_HOMEASSISTANT_START, + EVENT_HOMEASSISTANT_STOP) +from homeassistant.helpers import config_validation as cv + +REQUIREMENTS = ["aqualogic==1.0"] + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = "aqualogic" +UPDATE_TOPIC = DOMAIN + "_update" +CONF_UNIT = "unit" +RECONNECT_INTERVAL = timedelta(seconds=10) + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_PORT): cv.port + }), +}, extra=vol.ALLOW_EXTRA) + + +def setup(hass, config): + """Set up AquaLogic platform.""" + host = config[DOMAIN][CONF_HOST] + port = config[DOMAIN][CONF_PORT] + processor = AquaLogicProcessor(hass, host, port) + hass.data[DOMAIN] = processor + hass.bus.listen_once(EVENT_HOMEASSISTANT_START, + processor.start_listen) + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, + processor.shutdown) + _LOGGER.debug("AquaLogicProcessor %s:%i initialized", host, port) + return True + + +class AquaLogicProcessor(threading.Thread): + """AquaLogic event processor thread.""" + + def __init__(self, hass, host, port): + """Initialize the data object.""" + super().__init__(daemon=True) + self._hass = hass + self._host = host + self._port = port + self._shutdown = False + self._panel = None + + def start_listen(self, event): + """Start event-processing thread.""" + _LOGGER.debug("Event processing thread started") + self.start() + + def shutdown(self, event): + """Signal shutdown of processing event.""" + _LOGGER.debug("Event processing signaled exit") + self._shutdown = True + + def data_changed(self, panel): + """Aqualogic data changed callback.""" + self._hass.helpers.dispatcher.dispatcher_send(UPDATE_TOPIC) + + def run(self): + """Event thread.""" + from aqualogic.core import AquaLogic + + while True: + self._panel = AquaLogic() + self._panel.connect(self._host, self._port) + self._panel.process(self.data_changed) + + if self._shutdown: + return + + _LOGGER.error("Connection to %s:%d lost", + self._host, self._port) + time.sleep(RECONNECT_INTERVAL.seconds) + + @property + def panel(self): + """Retrieve the AquaLogic object.""" + return self._panel diff --git a/homeassistant/components/sensor/aqualogic.py b/homeassistant/components/sensor/aqualogic.py new file mode 100644 index 00000000000..f10fd05b83f --- /dev/null +++ b/homeassistant/components/sensor/aqualogic.py @@ -0,0 +1,111 @@ +""" +Support for AquaLogic sensors. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.aqualogic/ +""" +import logging + +import voluptuous as vol + +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import (CONF_MONITORED_CONDITIONS, + TEMP_CELSIUS, TEMP_FAHRENHEIT) +from homeassistant.core import callback +from homeassistant.helpers.entity import Entity +import homeassistant.components.aqualogic as aq +import homeassistant.helpers.config_validation as cv + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['aqualogic'] + +TEMP_UNITS = [TEMP_CELSIUS, TEMP_FAHRENHEIT] +PERCENT_UNITS = ['%', '%'] +SALT_UNITS = ['g/L', 'PPM'] +WATT_UNITS = ['W', 'W'] +NO_UNITS = [None, None] + +# sensor_type [ description, unit, icon ] +# sensor_type corresponds to property names in aqualogic.core.AquaLogic +SENSOR_TYPES = { + 'air_temp': ['Air Temperature', TEMP_UNITS, 'mdi:thermometer'], + 'pool_temp': ['Pool Temperature', TEMP_UNITS, 'mdi:oil-temperature'], + 'spa_temp': ['Spa Temperature', TEMP_UNITS, 'mdi:oil-temperature'], + 'pool_chlorinator': ['Pool Chlorinator', PERCENT_UNITS, 'mdi:gauge'], + 'spa_chlorinator': ['Spa Chlorinator', PERCENT_UNITS, 'mdi:gauge'], + 'salt_level': ['Salt Level', SALT_UNITS, 'mdi:gauge'], + 'pump_speed': ['Pump Speed', PERCENT_UNITS, 'mdi:speedometer'], + 'pump_power': ['Pump Power', WATT_UNITS, 'mdi:gauge'], + 'status': ['Status', NO_UNITS, 'mdi:alert'] +} + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_MONITORED_CONDITIONS, default=list(SENSOR_TYPES)): + vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]) +}) + + +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): + """Set up the sensor platform.""" + sensors = [] + + processor = hass.data[aq.DOMAIN] + for sensor_type in config.get(CONF_MONITORED_CONDITIONS): + sensors.append(AquaLogicSensor(processor, sensor_type)) + + async_add_entities(sensors) + + +class AquaLogicSensor(Entity): + """Sensor implementation for the AquaLogic component.""" + + def __init__(self, processor, sensor_type): + """Initialize sensor.""" + self._processor = processor + self._type = sensor_type + self._state = None + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + @property + def name(self): + """Return the name of the sensor.""" + return "AquaLogic {}".format(SENSOR_TYPES[self._type][0]) + + @property + def unit_of_measurement(self): + """Return the unit of measurement the value is expressed in.""" + panel = self._processor.panel + if panel is None: + return None + if panel.is_metric: + return SENSOR_TYPES[self._type][1][0] + return SENSOR_TYPES[self._type][1][1] + + @property + def should_poll(self): + """Return the polling state.""" + return False + + @property + def icon(self): + """Icon to use in the frontend, if any.""" + return SENSOR_TYPES[self._type][2] + + async def async_added_to_hass(self): + """Register callbacks.""" + self.hass.helpers.dispatcher.async_dispatcher_connect( + aq.UPDATE_TOPIC, self.async_update_callback) + + @callback + def async_update_callback(self): + """Update callback.""" + panel = self._processor.panel + if panel is not None: + self._state = getattr(panel, self._type) + self.async_schedule_update_ha_state() diff --git a/homeassistant/components/switch/aqualogic.py b/homeassistant/components/switch/aqualogic.py new file mode 100644 index 00000000000..48c4702aca0 --- /dev/null +++ b/homeassistant/components/switch/aqualogic.py @@ -0,0 +1,114 @@ +""" +Support for AquaLogic switches. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/switch.aqualogic/ +""" +import logging + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.core import callback +import homeassistant.components.aqualogic as aq +from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA +from homeassistant.const import (CONF_MONITORED_CONDITIONS) + +DEPENDENCIES = ['aqualogic'] + +_LOGGER = logging.getLogger(__name__) + +SWITCH_TYPES = { + 'lights': 'Lights', + 'filter': 'Filter', + 'filter_low_speed': 'Filter Low Speed', + 'aux_1': 'Aux 1', + 'aux_2': 'Aux 2', + 'aux_3': 'Aux 3', + 'aux_4': 'Aux 4', + 'aux_5': 'Aux 5', + 'aux_6': 'Aux 6', + 'aux_7': 'Aux 7', +} + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_MONITORED_CONDITIONS, default=list(SWITCH_TYPES)): + vol.All(cv.ensure_list, [vol.In(SWITCH_TYPES)]), +}) + + +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): + """Set up the switch platform.""" + switches = [] + + processor = hass.data[aq.DOMAIN] + for switch_type in config.get(CONF_MONITORED_CONDITIONS): + switches.append(AquaLogicSwitch(processor, switch_type)) + + async_add_entities(switches) + + +class AquaLogicSwitch(SwitchDevice): + """Switch implementation for the AquaLogic component.""" + + def __init__(self, processor, switch_type): + """Initialize switch.""" + from aqualogic.core import States + self._processor = processor + self._type = switch_type + self._state_name = { + 'lights': States.LIGHTS, + 'filter': States.FILTER, + 'filter_low_speed': States.FILTER_LOW_SPEED, + 'aux_1': States.AUX_1, + 'aux_2': States.AUX_2, + 'aux_3': States.AUX_3, + 'aux_4': States.AUX_4, + 'aux_5': States.AUX_5, + 'aux_6': States.AUX_6, + 'aux_7': States.AUX_7 + }[switch_type] + + @property + def name(self): + """Return the name of the switch.""" + return "AquaLogic {}".format(SWITCH_TYPES[self._type]) + + @property + def should_poll(self): + """Return the polling state.""" + return False + + @property + def is_on(self): + """Return true if device is on.""" + panel = self._processor.panel + if panel is None: + return False + state = panel.get_state(self._state_name) + return state + + def turn_on(self, **kwargs): + """Turn the device on.""" + panel = self._processor.panel + if panel is None: + return + panel.set_state(self._state_name, True) + + def turn_off(self, **kwargs): + """Turn the device off.""" + panel = self._processor.panel + if panel is None: + return + panel.set_state(self._state_name, False) + + async def async_added_to_hass(self): + """Register callbacks.""" + self.hass.helpers.dispatcher.async_dispatcher_connect( + aq.UPDATE_TOPIC, self.async_update_callback) + + @callback + def async_update_callback(self): + """Update callback.""" + self.async_schedule_update_ha_state() diff --git a/requirements_all.txt b/requirements_all.txt index b1d8f5dc4f3..0225e78ab9b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -139,6 +139,9 @@ apcaccess==0.0.13 # homeassistant.components.notify.apns apns2==0.3.0 +# homeassistant.components.aqualogic +aqualogic==1.0 + # homeassistant.components.asterisk_mbox asterisk_mbox==0.5.0 From 8e276295eb8eb131d461c9e837c9e9c365eef47e Mon Sep 17 00:00:00 2001 From: Mathieu Velten Date: Tue, 2 Oct 2018 09:14:07 +0200 Subject: [PATCH 148/247] Update pynetgear to 0.4.2 (fix #14752) (#17064) --- homeassistant/components/device_tracker/netgear.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/device_tracker/netgear.py b/homeassistant/components/device_tracker/netgear.py index 87be70b2040..2e1b96dffad 100644 --- a/homeassistant/components/device_tracker/netgear.py +++ b/homeassistant/components/device_tracker/netgear.py @@ -15,7 +15,7 @@ from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_PORT, CONF_SSL, CONF_DEVICES, CONF_EXCLUDE) -REQUIREMENTS = ['pynetgear==0.4.1'] +REQUIREMENTS = ['pynetgear==0.4.2'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 0225e78ab9b..f39d50202ab 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1000,7 +1000,7 @@ pymysensors==0.17.0 pynello==1.5.1 # homeassistant.components.device_tracker.netgear -pynetgear==0.4.1 +pynetgear==0.4.2 # homeassistant.components.switch.netio pynetio==0.1.6 From 166748134262952a17585fa4d98be411e523bec4 Mon Sep 17 00:00:00 2001 From: Jason Hu Date: Tue, 2 Oct 2018 00:21:02 -0700 Subject: [PATCH 149/247] Use Protractor loop in Windows (#17061) * Use Protractor loop in Windows * Add import sys --- homeassistant/util/async_.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/util/async_.py b/homeassistant/util/async_.py index 04456b8cb2f..61fbb60a24f 100644 --- a/homeassistant/util/async_.py +++ b/homeassistant/util/async_.py @@ -2,6 +2,7 @@ import concurrent.futures import threading import logging +import sys from asyncio import coroutines from asyncio.events import AbstractEventLoop from asyncio.futures import Future @@ -22,7 +23,10 @@ except AttributeError: def asyncio_run(main: Awaitable[_T], *, debug: bool = False) -> _T: """Minimal re-implementation of asyncio.run (since 3.7).""" - loop = asyncio.new_event_loop() + if sys.platform == 'win32': + loop = asyncio.ProactorEventLoop() + else: + loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) loop.set_debug(debug) try: From 1decba005216c2cfe8e53ef1c6bb49bac21ef4a0 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 2 Oct 2018 09:55:37 +0200 Subject: [PATCH 150/247] Proactor policy fix (#17066) * Proactor policy fix * Backport Proactor policy for None: """Attempt to use uvloop.""" import asyncio + from asyncio.events import BaseDefaultEventLoopPolicy + + policy = None if sys.platform == 'win32': - asyncio.set_event_loop(asyncio.ProactorEventLoop()) + if hasattr(asyncio, 'WindowsProactorEventLoopPolicy'): + policy = asyncio.WindowsProactorEventLoopPolicy() + else: + class ProactorPolicy(BaseDefaultEventLoopPolicy): + """Event loop policy to create proactor loops.""" + + _loop_factory = asyncio.ProactorEventLoop + + policy = ProactorPolicy() else: try: import uvloop except ImportError: pass else: - asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) + policy = uvloop.EventLoopPolicy() + + if policy is not None: + asyncio.set_event_loop_policy(policy) def validate_python() -> None: diff --git a/homeassistant/util/async_.py b/homeassistant/util/async_.py index 61fbb60a24f..04456b8cb2f 100644 --- a/homeassistant/util/async_.py +++ b/homeassistant/util/async_.py @@ -2,7 +2,6 @@ import concurrent.futures import threading import logging -import sys from asyncio import coroutines from asyncio.events import AbstractEventLoop from asyncio.futures import Future @@ -23,10 +22,7 @@ except AttributeError: def asyncio_run(main: Awaitable[_T], *, debug: bool = False) -> _T: """Minimal re-implementation of asyncio.run (since 3.7).""" - if sys.platform == 'win32': - loop = asyncio.ProactorEventLoop() - else: - loop = asyncio.new_event_loop() + loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) loop.set_debug(debug) try: From b0b3620b2b559da564b25ef86b1a2990c9e86b3f Mon Sep 17 00:00:00 2001 From: Nicko van Someren Date: Tue, 2 Oct 2018 04:16:43 -0400 Subject: [PATCH 151/247] Added working support for private storage (#16903) * Fixed file corruption bugs in private storage code. * Restoring fixed test case. * Implemented test suite for utils/json.py * Added new unit test cases for util/json.py * Dixed formatting nags * Fixed more nags from the Hound * Added doc strings to some very short functions * Fixing lint's complains about my choice of parts of speach. Sigh. * Moved atomic save operations down into util/json.py so that all benefit. Added extra clean-up code to ensure that temporary files are removed in case of errors. Updated emulated_hue unit tests to avoid errors. * Apparently 'e' is not allows as a variable name for an exception... --- homeassistant/util/json.py | 16 ++- tests/components/emulated_hue/test_init.py | 108 ++++++++++++--------- tests/helpers/test_storage.py | 1 + tests/util/test_json.py | 75 ++++++++++++++ 4 files changed, 151 insertions(+), 49 deletions(-) create mode 100644 tests/util/test_json.py diff --git a/homeassistant/util/json.py b/homeassistant/util/json.py index 40e26f80e91..0a2a2a1edf3 100644 --- a/homeassistant/util/json.py +++ b/homeassistant/util/json.py @@ -3,6 +3,8 @@ import logging from typing import Union, List, Dict import json +import os +from os import O_WRONLY, O_CREAT, O_TRUNC from homeassistant.exceptions import HomeAssistantError @@ -44,10 +46,14 @@ def save_json(filename: str, data: Union[List, Dict], Returns True on success. """ + tmp_filename = filename + "__TEMP__" try: json_data = json.dumps(data, sort_keys=True, indent=4) - with open(filename, 'w', encoding='utf-8') as fdesc: + mode = 0o600 if private else 0o644 + with open(os.open(tmp_filename, O_WRONLY | O_CREAT | O_TRUNC, mode), + 'w', encoding='utf-8') as fdesc: fdesc.write(json_data) + os.replace(tmp_filename, filename) except TypeError as error: _LOGGER.exception('Failed to serialize to JSON: %s', filename) @@ -56,3 +62,11 @@ def save_json(filename: str, data: Union[List, Dict], _LOGGER.exception('Saving JSON file failed: %s', filename) raise WriteError(error) + finally: + if os.path.exists(tmp_filename): + try: + os.remove(tmp_filename) + except OSError as err: + # If we are cleaning up then something else went wrong, so + # we should suppress likely follow-on errors in the cleanup + _LOGGER.error("JSON replacement cleanup failed: %s", err) diff --git a/tests/components/emulated_hue/test_init.py b/tests/components/emulated_hue/test_init.py index 2f443eb5d6e..9b0a5cd9052 100644 --- a/tests/components/emulated_hue/test_init.py +++ b/tests/components/emulated_hue/test_init.py @@ -1,14 +1,16 @@ """Test the Emulated Hue component.""" import json -from unittest.mock import patch, Mock, mock_open +from unittest.mock import patch, Mock, mock_open, MagicMock from homeassistant.components.emulated_hue import Config def test_config_google_home_entity_id_to_number(): """Test config adheres to the type.""" - conf = Config(Mock(), { + mock_hass = Mock() + mock_hass.config.path = MagicMock("path", return_value="test_path") + conf = Config(mock_hass, { 'type': 'google_home' }) @@ -16,29 +18,33 @@ def test_config_google_home_entity_id_to_number(): handle = mop() with patch('homeassistant.util.json.open', mop, create=True): - number = conf.entity_id_to_number('light.test') - assert number == '2' - assert handle.write.call_count == 1 - assert json.loads(handle.write.mock_calls[0][1][0]) == { - '1': 'light.test2', - '2': 'light.test', - } + with patch('homeassistant.util.json.os.open', return_value=0): + with patch('homeassistant.util.json.os.replace'): + number = conf.entity_id_to_number('light.test') + assert number == '2' + assert handle.write.call_count == 1 + assert json.loads(handle.write.mock_calls[0][1][0]) == { + '1': 'light.test2', + '2': 'light.test', + } - number = conf.entity_id_to_number('light.test') - assert number == '2' - assert handle.write.call_count == 1 + number = conf.entity_id_to_number('light.test') + assert number == '2' + assert handle.write.call_count == 1 - number = conf.entity_id_to_number('light.test2') - assert number == '1' - assert handle.write.call_count == 1 + number = conf.entity_id_to_number('light.test2') + assert number == '1' + assert handle.write.call_count == 1 - entity_id = conf.number_to_entity_id('1') - assert entity_id == 'light.test2' + entity_id = conf.number_to_entity_id('1') + assert entity_id == 'light.test2' def test_config_google_home_entity_id_to_number_altered(): """Test config adheres to the type.""" - conf = Config(Mock(), { + mock_hass = Mock() + mock_hass.config.path = MagicMock("path", return_value="test_path") + conf = Config(mock_hass, { 'type': 'google_home' }) @@ -46,29 +52,33 @@ def test_config_google_home_entity_id_to_number_altered(): handle = mop() with patch('homeassistant.util.json.open', mop, create=True): - number = conf.entity_id_to_number('light.test') - assert number == '22' - assert handle.write.call_count == 1 - assert json.loads(handle.write.mock_calls[0][1][0]) == { - '21': 'light.test2', - '22': 'light.test', - } + with patch('homeassistant.util.json.os.open', return_value=0): + with patch('homeassistant.util.json.os.replace'): + number = conf.entity_id_to_number('light.test') + assert number == '22' + assert handle.write.call_count == 1 + assert json.loads(handle.write.mock_calls[0][1][0]) == { + '21': 'light.test2', + '22': 'light.test', + } - number = conf.entity_id_to_number('light.test') - assert number == '22' - assert handle.write.call_count == 1 + number = conf.entity_id_to_number('light.test') + assert number == '22' + assert handle.write.call_count == 1 - number = conf.entity_id_to_number('light.test2') - assert number == '21' - assert handle.write.call_count == 1 + number = conf.entity_id_to_number('light.test2') + assert number == '21' + assert handle.write.call_count == 1 - entity_id = conf.number_to_entity_id('21') - assert entity_id == 'light.test2' + entity_id = conf.number_to_entity_id('21') + assert entity_id == 'light.test2' def test_config_google_home_entity_id_to_number_empty(): """Test config adheres to the type.""" - conf = Config(Mock(), { + mock_hass = Mock() + mock_hass.config.path = MagicMock("path", return_value="test_path") + conf = Config(mock_hass, { 'type': 'google_home' }) @@ -76,23 +86,25 @@ def test_config_google_home_entity_id_to_number_empty(): handle = mop() with patch('homeassistant.util.json.open', mop, create=True): - number = conf.entity_id_to_number('light.test') - assert number == '1' - assert handle.write.call_count == 1 - assert json.loads(handle.write.mock_calls[0][1][0]) == { - '1': 'light.test', - } + with patch('homeassistant.util.json.os.open', return_value=0): + with patch('homeassistant.util.json.os.replace'): + number = conf.entity_id_to_number('light.test') + assert number == '1' + assert handle.write.call_count == 1 + assert json.loads(handle.write.mock_calls[0][1][0]) == { + '1': 'light.test', + } - number = conf.entity_id_to_number('light.test') - assert number == '1' - assert handle.write.call_count == 1 + number = conf.entity_id_to_number('light.test') + assert number == '1' + assert handle.write.call_count == 1 - number = conf.entity_id_to_number('light.test2') - assert number == '2' - assert handle.write.call_count == 2 + number = conf.entity_id_to_number('light.test2') + assert number == '2' + assert handle.write.call_count == 2 - entity_id = conf.number_to_entity_id('2') - assert entity_id == 'light.test2' + entity_id = conf.number_to_entity_id('2') + assert entity_id == 'light.test2' def test_config_alexa_entity_id_to_number(): diff --git a/tests/helpers/test_storage.py b/tests/helpers/test_storage.py index 6cb75899d35..38b8a7cd380 100644 --- a/tests/helpers/test_storage.py +++ b/tests/helpers/test_storage.py @@ -15,6 +15,7 @@ from tests.common import async_fire_time_changed, mock_coro MOCK_VERSION = 1 MOCK_KEY = 'storage-test' MOCK_DATA = {'hello': 'world'} +MOCK_DATA2 = {'goodbye': 'cruel world'} @pytest.fixture diff --git a/tests/util/test_json.py b/tests/util/test_json.py new file mode 100644 index 00000000000..53f62682b5e --- /dev/null +++ b/tests/util/test_json.py @@ -0,0 +1,75 @@ +"""Test Home Assistant json utility functions.""" +import os +import unittest +import sys +from tempfile import mkdtemp + +from homeassistant.util.json import (SerializationError, + load_json, save_json) +from homeassistant.exceptions import HomeAssistantError + +# Test data that can be saved as JSON +TEST_JSON_A = {"a": 1, "B": "two"} +TEST_JSON_B = {"a": "one", "B": 2} +# Test data that can not be saved as JSON (keys must be strings) +TEST_BAD_OBJECT = {("A",): 1} +# Test data that can not be loaded as JSON +TEST_BAD_SERIALIED = "THIS IS NOT JSON\n" + + +class TestJSON(unittest.TestCase): + """Test util.json save and load.""" + + def setUp(self): + """Set up for tests.""" + self.tmp_dir = mkdtemp() + + def tearDown(self): + """Clean up after tests.""" + for fname in os.listdir(self.tmp_dir): + os.remove(os.path.join(self.tmp_dir, fname)) + os.rmdir(self.tmp_dir) + + def _path_for(self, leaf_name): + return os.path.join(self.tmp_dir, leaf_name+".json") + + def test_save_and_load(self): + """Test saving and loading back.""" + fname = self._path_for("test1") + save_json(fname, TEST_JSON_A) + data = load_json(fname) + self.assertEqual(data, TEST_JSON_A) + + # Skipped on Windows + @unittest.skipIf(sys.platform.startswith('win'), + "private permissions not supported on Windows") + def test_save_and_load_private(self): + """Test we can load private files and that they are protected.""" + fname = self._path_for("test2") + save_json(fname, TEST_JSON_A, private=True) + data = load_json(fname) + self.assertEqual(data, TEST_JSON_A) + stats = os.stat(fname) + self.assertEqual(stats.st_mode & 0o77, 0) + + def test_overwrite_and_reload(self): + """Test that we can overwrite an existing file and read back.""" + fname = self._path_for("test3") + save_json(fname, TEST_JSON_A) + save_json(fname, TEST_JSON_B) + data = load_json(fname) + self.assertEqual(data, TEST_JSON_B) + + def test_save_bad_data(self): + """Test error from trying to save unserialisable data.""" + fname = self._path_for("test4") + with self.assertRaises(SerializationError): + save_json(fname, TEST_BAD_OBJECT) + + def test_load_bad_data(self): + """Test error from trying to load unserialisable data.""" + fname = self._path_for("test5") + with open(fname, "w") as fh: + fh.write(TEST_BAD_SERIALIED) + with self.assertRaises(HomeAssistantError): + load_json(fname) From 13af61e103f2e106d42660357e26d5ec3773a60e Mon Sep 17 00:00:00 2001 From: Malte Franken Date: Tue, 2 Oct 2018 18:20:51 +1000 Subject: [PATCH 152/247] GeoRSS events sensor refactored (#16939) * refactored geo_rss_events sensor to make use of new georss-client library that handles the communication with the rss feed * fixed lint error --- .../components/sensor/geo_rss_events.py | 150 +++--------- requirements_all.txt | 7 +- requirements_test_all.txt | 7 +- script/gen_requirements_all.py | 1 + .../components/sensor/test_geo_rss_events.py | 229 +++++++++--------- tests/fixtures/geo_rss_events.xml | 76 ------ 6 files changed, 154 insertions(+), 316 deletions(-) delete mode 100644 tests/fixtures/geo_rss_events.xml diff --git a/homeassistant/components/sensor/geo_rss_events.py b/homeassistant/components/sensor/geo_rss_events.py index 1ba0ce2e065..2a9041df357 100644 --- a/homeassistant/components/sensor/geo_rss_events.py +++ b/homeassistant/components/sensor/geo_rss_events.py @@ -9,7 +9,6 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.geo_rss_events/ """ import logging -from collections import namedtuple from datetime import timedelta import voluptuous as vol @@ -19,9 +18,8 @@ from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( STATE_UNKNOWN, CONF_UNIT_OF_MEASUREMENT, CONF_NAME, CONF_RADIUS, CONF_URL) from homeassistant.helpers.entity import Entity -from homeassistant.util import Throttle -REQUIREMENTS = ['feedparser==5.2.1', 'haversine==0.4.5'] +REQUIREMENTS = ['georss_client==0.1'] _LOGGER = logging.getLogger(__name__) @@ -38,9 +36,6 @@ DEFAULT_UNIT_OF_MEASUREMENT = 'Events' DOMAIN = 'geo_rss_events' -# Minimum time between updates from the source. -MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=1) - SCAN_INTERVAL = timedelta(minutes=5) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ @@ -67,18 +62,17 @@ def setup_platform(hass, config, add_entities, discovery_info=None): _LOGGER.debug("latitude=%s, longitude=%s, url=%s, radius=%s", home_latitude, home_longitude, url, radius_in_km) - # Initialise update service. - data = GeoRssServiceData(home_latitude, home_longitude, url, radius_in_km) - data.update() - # Create all sensors based on categories. devices = [] if not categories: - device = GeoRssServiceSensor(None, data, name, unit_of_measurement) + device = GeoRssServiceSensor((home_latitude, home_longitude), url, + radius_in_km, None, name, + unit_of_measurement) devices.append(device) else: for category in categories: - device = GeoRssServiceSensor(category, data, name, + device = GeoRssServiceSensor((home_latitude, home_longitude), url, + radius_in_km, category, name, unit_of_measurement) devices.append(device) add_entities(devices, True) @@ -87,14 +81,18 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class GeoRssServiceSensor(Entity): """Representation of a Sensor.""" - def __init__(self, category, data, service_name, unit_of_measurement): + def __init__(self, home_coordinates, url, radius, category, service_name, + unit_of_measurement): """Initialize the sensor.""" self._category = category - self._data = data self._service_name = service_name self._state = STATE_UNKNOWN self._state_attributes = None self._unit_of_measurement = unit_of_measurement + from georss_client.generic_feed import GenericFeed + self._feed = GenericFeed(home_coordinates, url, filter_radius=radius, + filter_categories=None if not category + else [category]) @property def name(self): @@ -125,115 +123,25 @@ class GeoRssServiceSensor(Entity): def update(self): """Update this sensor from the GeoRSS service.""" - _LOGGER.debug("About to update sensor %s", self.entity_id) - self._data.update() - # If no events were found due to an error then just set state to zero. - if self._data.events is None: - self._state = 0 - else: - if self._category is None: - # Add all events regardless of category. - my_events = self._data.events - else: - # Only keep events that belong to sensor's category. - my_events = [event for event in self._data.events if - event[ATTR_CATEGORY] == self._category] + import georss_client + status, feed_entries = self._feed.update() + if status == georss_client.UPDATE_OK: _LOGGER.debug("Adding events to sensor %s: %s", self.entity_id, - my_events) - self._state = len(my_events) + feed_entries) + self._state = len(feed_entries) # And now compute the attributes from the filtered events. matrix = {} - for event in my_events: - matrix[event[ATTR_TITLE]] = '{:.0f}km'.format( - event[ATTR_DISTANCE]) + for entry in feed_entries: + matrix[entry.title] = '{:.0f}km'.format( + entry.distance_to_home) self._state_attributes = matrix - - -class GeoRssServiceData: - """Provide access to GeoRSS feed and stores the latest data.""" - - def __init__(self, home_latitude, home_longitude, url, radius_in_km): - """Initialize the update service.""" - self._home_coordinates = [home_latitude, home_longitude] - self._url = url - self._radius_in_km = radius_in_km - self.events = None - - @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self): - """Retrieve data from GeoRSS feed and store events.""" - import feedparser - feed_data = feedparser.parse(self._url) - if not feed_data: - _LOGGER.error("Error fetching feed data from %s", self._url) + elif status == georss_client.UPDATE_OK_NO_DATA: + _LOGGER.debug("Update successful, but no data received from %s", + self._feed) + # Don't change the state or state attributes. else: - events = self.filter_entries(feed_data) - self.events = events - - def filter_entries(self, feed_data): - """Filter entries by distance from home coordinates.""" - events = [] - _LOGGER.debug("%s entri(es) available in feed %s", - len(feed_data.entries), self._url) - for entry in feed_data.entries: - geometry = None - if hasattr(entry, 'where'): - geometry = entry.where - elif hasattr(entry, 'geo_lat') and hasattr(entry, 'geo_long'): - coordinates = (float(entry.geo_long), float(entry.geo_lat)) - point = namedtuple('Point', ['type', 'coordinates']) - geometry = point('Point', coordinates) - if geometry: - distance = self.calculate_distance_to_geometry(geometry) - if distance <= self._radius_in_km: - event = { - ATTR_CATEGORY: None if not hasattr( - entry, 'category') else entry.category, - ATTR_TITLE: None if not hasattr( - entry, 'title') else entry.title, - ATTR_DISTANCE: distance - } - events.append(event) - _LOGGER.debug("%s events found nearby", len(events)) - return events - - def calculate_distance_to_geometry(self, geometry): - """Calculate the distance between HA and provided geometry.""" - distance = float("inf") - if geometry.type == 'Point': - distance = self.calculate_distance_to_point(geometry) - elif geometry.type == 'Polygon': - distance = self.calculate_distance_to_polygon( - geometry.coordinates[0]) - else: - _LOGGER.warning("Not yet implemented: %s", geometry.type) - return distance - - def calculate_distance_to_point(self, point): - """Calculate the distance between HA and the provided point.""" - # Swap coordinates to match: (lat, lon). - coordinates = (point.coordinates[1], point.coordinates[0]) - return self.calculate_distance_to_coords(coordinates) - - def calculate_distance_to_coords(self, coordinates): - """Calculate the distance between HA and the provided coordinates.""" - # Expecting coordinates in format: (lat, lon). - from haversine import haversine - distance = haversine(coordinates, self._home_coordinates) - _LOGGER.debug("Distance from %s to %s: %s km", self._home_coordinates, - coordinates, distance) - return distance - - def calculate_distance_to_polygon(self, polygon): - """Calculate the distance between HA and the provided polygon.""" - distance = float("inf") - # Calculate distance from polygon by calculating the distance - # to each point of the polygon but not to each edge of the - # polygon; should be good enough - for polygon_point in polygon: - coordinates = (polygon_point[1], polygon_point[0]) - distance = min(distance, - self.calculate_distance_to_coords(coordinates)) - _LOGGER.debug("Distance from %s to %s: %s km", self._home_coordinates, - polygon, distance) - return distance + _LOGGER.warning("Update not successful, no data received from %s", + self._feed) + # If no events were found due to an error then just set state to + # zero. + self._state = 0 diff --git a/requirements_all.txt b/requirements_all.txt index f39d50202ab..c93c8a8618c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -356,7 +356,6 @@ fastdotcom==0.0.3 fedexdeliverymanager==1.0.6 # homeassistant.components.feedreader -# homeassistant.components.sensor.geo_rss_events feedparser==5.2.1 # homeassistant.components.sensor.fints @@ -397,6 +396,9 @@ geizhals==0.0.7 # homeassistant.components.geo_location.geo_json_events geojson_client==0.1 +# homeassistant.components.sensor.geo_rss_events +georss_client==0.1 + # homeassistant.components.sensor.gitter gitterpy==0.1.7 @@ -433,9 +435,6 @@ habitipy==0.2.0 # homeassistant.components.hangouts hangups==0.4.5 -# homeassistant.components.sensor.geo_rss_events -haversine==0.4.5 - # homeassistant.components.mqtt.server hbmqtt==0.9.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 04cec3b1e6f..80067e607a9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -57,7 +57,6 @@ ephem==3.7.6.0 evohomeclient==0.2.7 # homeassistant.components.feedreader -# homeassistant.components.sensor.geo_rss_events feedparser==5.2.1 # homeassistant.components.sensor.foobot @@ -69,15 +68,15 @@ gTTS-token==1.1.2 # homeassistant.components.geo_location.geo_json_events geojson_client==0.1 +# homeassistant.components.sensor.geo_rss_events +georss_client==0.1 + # homeassistant.components.ffmpeg ha-ffmpeg==1.9 # homeassistant.components.hangouts hangups==0.4.5 -# homeassistant.components.sensor.geo_rss_events -haversine==0.4.5 - # homeassistant.components.mqtt.server hbmqtt==0.9.4 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 7493e523273..c6e36c5c83e 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -51,6 +51,7 @@ TEST_REQUIREMENTS = ( 'foobot_async', 'gTTS-token', 'geojson_client', + 'georss_client', 'hangups', 'HAP-python', 'ha-ffmpeg', diff --git a/tests/components/sensor/test_geo_rss_events.py b/tests/components/sensor/test_geo_rss_events.py index cc57c801430..21538d458bc 100644 --- a/tests/components/sensor/test_geo_rss_events.py +++ b/tests/components/sensor/test_geo_rss_events.py @@ -2,26 +2,38 @@ import unittest from unittest import mock import sys +from unittest.mock import MagicMock, patch -import feedparser import pytest +from homeassistant.components import sensor +from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, ATTR_FRIENDLY_NAME, \ + EVENT_HOMEASSISTANT_START, ATTR_ICON from homeassistant.setup import setup_component -from tests.common import load_fixture, get_test_home_assistant +from tests.common import get_test_home_assistant, \ + assert_setup_component, fire_time_changed import homeassistant.components.sensor.geo_rss_events as geo_rss_events +import homeassistant.util.dt as dt_util URL = 'http://geo.rss.local/geo_rss_events.xml' VALID_CONFIG_WITH_CATEGORIES = { - 'platform': 'geo_rss_events', - geo_rss_events.CONF_URL: URL, - geo_rss_events.CONF_CATEGORIES: [ - 'Category 1', - 'Category 2' + sensor.DOMAIN: [ + { + 'platform': 'geo_rss_events', + geo_rss_events.CONF_URL: URL, + geo_rss_events.CONF_CATEGORIES: [ + 'Category 1' + ] + } ] } -VALID_CONFIG_WITHOUT_CATEGORIES = { - 'platform': 'geo_rss_events', - geo_rss_events.CONF_URL: URL +VALID_CONFIG = { + sensor.DOMAIN: [ + { + 'platform': 'geo_rss_events', + geo_rss_events.CONF_URL: URL + } + ] } @@ -34,119 +46,114 @@ class TestGeoRssServiceUpdater(unittest.TestCase): def setUp(self): """Initialize values for this testcase class.""" self.hass = get_test_home_assistant() - self.config = VALID_CONFIG_WITHOUT_CATEGORIES + # self.config = VALID_CONFIG_WITHOUT_CATEGORIES def tearDown(self): """Stop everything that was started.""" self.hass.stop() - @mock.patch('feedparser.parse', return_value=feedparser.parse("")) - def test_setup_with_categories(self, mock_parse): - """Test the general setup of this sensor.""" - self.config = VALID_CONFIG_WITH_CATEGORIES - self.assertTrue( - setup_component(self.hass, 'sensor', {'sensor': self.config})) - self.assertIsNotNone( - self.hass.states.get('sensor.event_service_category_1')) - self.assertIsNotNone( - self.hass.states.get('sensor.event_service_category_2')) + @staticmethod + def _generate_mock_feed_entry(external_id, title, distance_to_home, + coordinates, category): + """Construct a mock feed entry for testing purposes.""" + feed_entry = MagicMock() + feed_entry.external_id = external_id + feed_entry.title = title + feed_entry.distance_to_home = distance_to_home + feed_entry.coordinates = coordinates + feed_entry.category = category + return feed_entry - @mock.patch('feedparser.parse', return_value=feedparser.parse("")) - def test_setup_without_categories(self, mock_parse): - """Test the general setup of this sensor.""" - self.assertTrue( - setup_component(self.hass, 'sensor', {'sensor': self.config})) - self.assertIsNotNone(self.hass.states.get('sensor.event_service_any')) + @mock.patch('georss_client.generic_feed.GenericFeed') + def test_setup(self, mock_feed): + """Test the general setup of the platform.""" + # Set up some mock feed entries for this test. + mock_entry_1 = self._generate_mock_feed_entry('1234', 'Title 1', 15.5, + (-31.0, 150.0), + 'Category 1') + mock_entry_2 = self._generate_mock_feed_entry('2345', 'Title 2', 20.5, + (-31.1, 150.1), + 'Category 1') + mock_feed.return_value.update.return_value = 'OK', [mock_entry_1, + mock_entry_2] - def setup_data(self, url='url'): - """Set up data object for use by sensors.""" - home_latitude = -33.865 - home_longitude = 151.209444 - radius_in_km = 500 - data = geo_rss_events.GeoRssServiceData(home_latitude, - home_longitude, url, - radius_in_km) - return data + utcnow = dt_util.utcnow() + # Patching 'utcnow' to gain more control over the timed update. + with patch('homeassistant.util.dt.utcnow', return_value=utcnow): + with assert_setup_component(1, sensor.DOMAIN): + self.assertTrue(setup_component(self.hass, sensor.DOMAIN, + VALID_CONFIG)) + # Artificially trigger update. + self.hass.bus.fire(EVENT_HOMEASSISTANT_START) + # Collect events. + self.hass.block_till_done() - def test_update_sensor_with_category(self): - """Test updating sensor object.""" - raw_data = load_fixture('geo_rss_events.xml') - # Loading raw data from fixture and plug in to data object as URL - # works since the third-party feedparser library accepts a URL - # as well as the actual data. - data = self.setup_data(raw_data) - category = "Category 1" - name = "Name 1" - unit_of_measurement = "Unit 1" - sensor = geo_rss_events.GeoRssServiceSensor(category, - data, name, - unit_of_measurement) + all_states = self.hass.states.all() + assert len(all_states) == 1 - sensor.update() - assert sensor.name == "Name 1 Category 1" - assert sensor.unit_of_measurement == "Unit 1" - assert sensor.icon == "mdi:alert" - assert len(sensor._data.events) == 4 - assert sensor.state == 1 - assert sensor.device_state_attributes == {'Title 1': "117km"} - # Check entries of first hit - assert sensor._data.events[0][geo_rss_events.ATTR_TITLE] == "Title 1" - assert sensor._data.events[0][ - geo_rss_events.ATTR_CATEGORY] == "Category 1" - self.assertAlmostEqual(sensor._data.events[0][ - geo_rss_events.ATTR_DISTANCE], 116.586, 0) + state = self.hass.states.get("sensor.event_service_any") + self.assertIsNotNone(state) + assert state.name == "Event Service Any" + assert int(state.state) == 2 + assert state.attributes == { + ATTR_FRIENDLY_NAME: "Event Service Any", + ATTR_UNIT_OF_MEASUREMENT: "Events", + ATTR_ICON: "mdi:alert", + "Title 1": "16km", "Title 2": "20km"} - def test_update_sensor_without_category(self): - """Test updating sensor object.""" - raw_data = load_fixture('geo_rss_events.xml') - data = self.setup_data(raw_data) - category = None - name = "Name 2" - unit_of_measurement = "Unit 2" - sensor = geo_rss_events.GeoRssServiceSensor(category, - data, name, - unit_of_measurement) + # Simulate an update - empty data, but successful update, + # so no changes to entities. + mock_feed.return_value.update.return_value = 'OK_NO_DATA', None + fire_time_changed(self.hass, utcnow + + geo_rss_events.SCAN_INTERVAL) + self.hass.block_till_done() - sensor.update() - assert sensor.name == "Name 2 Any" - assert sensor.unit_of_measurement == "Unit 2" - assert sensor.icon == "mdi:alert" - assert len(sensor._data.events) == 4 - assert sensor.state == 4 - assert sensor.device_state_attributes == {'Title 1': "117km", - 'Title 2': "302km", - 'Title 3': "204km", - 'Title 6': "48km"} + all_states = self.hass.states.all() + assert len(all_states) == 1 + state = self.hass.states.get("sensor.event_service_any") + assert int(state.state) == 2 - def test_update_sensor_without_data(self): - """Test updating sensor object.""" - data = self.setup_data() - category = None - name = "Name 3" - unit_of_measurement = "Unit 3" - sensor = geo_rss_events.GeoRssServiceSensor(category, - data, name, - unit_of_measurement) + # Simulate an update - empty data, removes all entities + mock_feed.return_value.update.return_value = 'ERROR', None + fire_time_changed(self.hass, utcnow + + 2 * geo_rss_events.SCAN_INTERVAL) + self.hass.block_till_done() - sensor.update() - assert sensor.name == "Name 3 Any" - assert sensor.unit_of_measurement == "Unit 3" - assert sensor.icon == "mdi:alert" - assert len(sensor._data.events) == 0 - assert sensor.state == 0 + all_states = self.hass.states.all() + assert len(all_states) == 1 + state = self.hass.states.get("sensor.event_service_any") + assert int(state.state) == 0 - @mock.patch('feedparser.parse', return_value=None) - def test_update_sensor_with_none_result(self, parse_function): - """Test updating sensor object.""" - data = self.setup_data("http://invalid.url/") - category = None - name = "Name 4" - unit_of_measurement = "Unit 4" - sensor = geo_rss_events.GeoRssServiceSensor(category, - data, name, - unit_of_measurement) + @mock.patch('georss_client.generic_feed.GenericFeed') + def test_setup_with_categories(self, mock_feed): + """Test the general setup of the platform.""" + # Set up some mock feed entries for this test. + mock_entry_1 = self._generate_mock_feed_entry('1234', 'Title 1', 15.5, + (-31.0, 150.0), + 'Category 1') + mock_entry_2 = self._generate_mock_feed_entry('2345', 'Title 2', 20.5, + (-31.1, 150.1), + 'Category 1') + mock_feed.return_value.update.return_value = 'OK', [mock_entry_1, + mock_entry_2] - sensor.update() - assert sensor.name == "Name 4 Any" - assert sensor.unit_of_measurement == "Unit 4" - assert sensor.state == 0 + with assert_setup_component(1, sensor.DOMAIN): + self.assertTrue(setup_component(self.hass, sensor.DOMAIN, + VALID_CONFIG_WITH_CATEGORIES)) + # Artificially trigger update. + self.hass.bus.fire(EVENT_HOMEASSISTANT_START) + # Collect events. + self.hass.block_till_done() + + all_states = self.hass.states.all() + assert len(all_states) == 1 + + state = self.hass.states.get("sensor.event_service_category_1") + self.assertIsNotNone(state) + assert state.name == "Event Service Category 1" + assert int(state.state) == 2 + assert state.attributes == { + ATTR_FRIENDLY_NAME: "Event Service Category 1", + ATTR_UNIT_OF_MEASUREMENT: "Events", + ATTR_ICON: "mdi:alert", + "Title 1": "16km", "Title 2": "20km"} diff --git a/tests/fixtures/geo_rss_events.xml b/tests/fixtures/geo_rss_events.xml deleted file mode 100644 index 212994756d2..00000000000 --- a/tests/fixtures/geo_rss_events.xml +++ /dev/null @@ -1,76 +0,0 @@ - - - - - - Title 1 - Description 1 - Category 1 - Sun, 30 Jul 2017 09:00:00 UTC - GUID 1 - -32.916667 151.75 - - - - Title 2 - Description 2 - Category 2 - Sun, 30 Jul 2017 09:05:00 GMT - GUID 2 - 148.601111 - -32.256944 - - - - Title 3 - Description 3 - Category 3 - Sun, 30 Jul 2017 09:05:00 GMT - GUID 3 - - -33.283333 149.1 - -33.2999997 149.1 - -33.2999997 149.1166663888889 - -33.283333 149.1166663888889 - -33.283333 149.1 - - - - - Title 4 - Description 4 - Category 4 - Sun, 30 Jul 2017 09:15:00 GMT - GUID 4 - 52.518611 13.408333 - - - - Title 5 - Description 5 - Category 5 - Sun, 30 Jul 2017 09:20:00 GMT - GUID 5 - - - - - Title 6 - Description 6 - Category 6 - 2017-07-30T09:25:00.000Z - Link 6 - -33.75801 150.70544 - - - - Title 1 - Description 1 - Category 1 - Sun, 30 Jul 2017 09:00:00 UTC - GUID 1 - 45.256 -110.45 46.46 -109.48 43.84 -109.86 - - - \ No newline at end of file From 90f71261c5d7a73c81b48a701a2037c81c9659c8 Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Tue, 2 Oct 2018 10:23:37 +0200 Subject: [PATCH 153/247] Remove service helper (8) (#17055) * Updated keyboard * Updated microsoft_face * Updated ffmpeg * Updated logger * Updated components/__init__.py --- homeassistant/components/__init__.py | 49 ----------- homeassistant/components/config/customize.py | 9 +- homeassistant/components/ffmpeg.py | 21 ----- homeassistant/components/keyboard.py | 30 ------- homeassistant/components/logger.py | 5 -- homeassistant/components/microsoft_face.py | 37 -------- tests/components/test_ffmpeg.py | 42 ++++++++- tests/components/test_init.py | 91 +++++++++++++++++--- tests/components/test_microsoft_face.py | 76 ++++++++++++++-- 9 files changed, 193 insertions(+), 167 deletions(-) diff --git a/homeassistant/components/__init__.py b/homeassistant/components/__init__.py index e8994592d40..e2701ee37f1 100644 --- a/homeassistant/components/__init__.py +++ b/homeassistant/components/__init__.py @@ -59,55 +59,6 @@ def is_on(hass, entity_id=None): return False -def turn_on(hass, entity_id=None, **service_data): - """Turn specified entity on if possible.""" - if entity_id is not None: - service_data[ATTR_ENTITY_ID] = entity_id - - hass.services.call(ha.DOMAIN, SERVICE_TURN_ON, service_data) - - -def turn_off(hass, entity_id=None, **service_data): - """Turn specified entity off.""" - if entity_id is not None: - service_data[ATTR_ENTITY_ID] = entity_id - - hass.services.call(ha.DOMAIN, SERVICE_TURN_OFF, service_data) - - -def toggle(hass, entity_id=None, **service_data): - """Toggle specified entity.""" - if entity_id is not None: - service_data[ATTR_ENTITY_ID] = entity_id - - hass.services.call(ha.DOMAIN, SERVICE_TOGGLE, service_data) - - -def stop(hass): - """Stop Home Assistant.""" - hass.services.call(ha.DOMAIN, SERVICE_HOMEASSISTANT_STOP) - - -def restart(hass): - """Stop Home Assistant.""" - hass.services.call(ha.DOMAIN, SERVICE_HOMEASSISTANT_RESTART) - - -def check_config(hass): - """Check the config files.""" - hass.services.call(ha.DOMAIN, SERVICE_CHECK_CONFIG) - - -def reload_core_config(hass): - """Reload the core config.""" - hass.services.call(ha.DOMAIN, SERVICE_RELOAD_CORE_CONFIG) - - -async def async_reload_core_config(hass): - """Reload the core config.""" - await hass.services.async_call(ha.DOMAIN, SERVICE_RELOAD_CORE_CONFIG) - - async def async_setup(hass: ha.HomeAssistant, config: dict) -> Awaitable[bool]: """Set up general services related to Home Assistant.""" async def async_handle_turn_service(service): diff --git a/homeassistant/components/config/customize.py b/homeassistant/components/config/customize.py index eee63005944..b7a8c9c070a 100644 --- a/homeassistant/components/config/customize.py +++ b/homeassistant/components/config/customize.py @@ -1,8 +1,9 @@ """Provide configuration end points for Customize.""" from homeassistant.components.config import EditKeyBasedConfigView -from homeassistant.components import async_reload_core_config +from homeassistant.components import SERVICE_RELOAD_CORE_CONFIG from homeassistant.config import DATA_CUSTOMIZE +from homeassistant.core import DOMAIN import homeassistant.helpers.config_validation as cv @@ -11,9 +12,13 @@ CONFIG_PATH = 'customize.yaml' async def async_setup(hass): """Set up the Customize config API.""" + async def hook(hass): + """post_write_hook for Config View that reloads groups.""" + await hass.services.async_call(DOMAIN, SERVICE_RELOAD_CORE_CONFIG) + hass.http.register_view(CustomizeConfigView( 'customize', 'config', CONFIG_PATH, cv.entity_id, dict, - post_write_hook=async_reload_core_config + post_write_hook=hook )) return True diff --git a/homeassistant/components/ffmpeg.py b/homeassistant/components/ffmpeg.py index 64915f8849c..f28dbd52336 100644 --- a/homeassistant/components/ffmpeg.py +++ b/homeassistant/components/ffmpeg.py @@ -54,27 +54,6 @@ SERVICE_FFMPEG_SCHEMA = vol.Schema({ }) -@callback -def async_start(hass, entity_id=None): - """Start a FFmpeg process on entity.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} - hass.async_add_job(hass.services.async_call(DOMAIN, SERVICE_START, data)) - - -@callback -def async_stop(hass, entity_id=None): - """Stop a FFmpeg process on entity.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} - hass.async_add_job(hass.services.async_call(DOMAIN, SERVICE_STOP, data)) - - -@callback -def async_restart(hass, entity_id=None): - """Restart a FFmpeg process on entity.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} - hass.async_add_job(hass.services.async_call(DOMAIN, SERVICE_RESTART, data)) - - async def async_setup(hass, config): """Set up the FFmpeg component.""" conf = config.get(DOMAIN, {}) diff --git a/homeassistant/components/keyboard.py b/homeassistant/components/keyboard.py index 697b5b6873c..58bb1fa5f42 100644 --- a/homeassistant/components/keyboard.py +++ b/homeassistant/components/keyboard.py @@ -18,36 +18,6 @@ DOMAIN = 'keyboard' TAP_KEY_SCHEMA = vol.Schema({}) -def volume_up(hass): - """Press the keyboard button for volume up.""" - hass.services.call(DOMAIN, SERVICE_VOLUME_UP) - - -def volume_down(hass): - """Press the keyboard button for volume down.""" - hass.services.call(DOMAIN, SERVICE_VOLUME_DOWN) - - -def volume_mute(hass): - """Press the keyboard button for muting volume.""" - hass.services.call(DOMAIN, SERVICE_VOLUME_MUTE) - - -def media_play_pause(hass): - """Press the keyboard button for play/pause.""" - hass.services.call(DOMAIN, SERVICE_MEDIA_PLAY_PAUSE) - - -def media_next_track(hass): - """Press the keyboard button for next track.""" - hass.services.call(DOMAIN, SERVICE_MEDIA_NEXT_TRACK) - - -def media_prev_track(hass): - """Press the keyboard button for prev track.""" - hass.services.call(DOMAIN, SERVICE_MEDIA_PREVIOUS_TRACK) - - def setup(hass, config): """Listen for keyboard events.""" # pylint: disable=import-error diff --git a/homeassistant/components/logger.py b/homeassistant/components/logger.py index 0baca2f341c..21ae7595ab8 100644 --- a/homeassistant/components/logger.py +++ b/homeassistant/components/logger.py @@ -47,11 +47,6 @@ CONFIG_SCHEMA = vol.Schema({ }, extra=vol.ALLOW_EXTRA) -def set_level(hass, logs): - """Set log level for components.""" - hass.services.call(DOMAIN, SERVICE_SET_LEVEL, logs) - - class HomeAssistantLogFilter(logging.Filter): """A log filter.""" diff --git a/homeassistant/components/microsoft_face.py b/homeassistant/components/microsoft_face.py index c06ed6bc8f3..9be2f8eadf5 100644 --- a/homeassistant/components/microsoft_face.py +++ b/homeassistant/components/microsoft_face.py @@ -69,43 +69,6 @@ SCHEMA_TRAIN_SERVICE = vol.Schema({ }) -def create_group(hass, name): - """Create a new person group.""" - data = {ATTR_NAME: name} - hass.services.call(DOMAIN, SERVICE_CREATE_GROUP, data) - - -def delete_group(hass, name): - """Delete a person group.""" - data = {ATTR_NAME: name} - hass.services.call(DOMAIN, SERVICE_DELETE_GROUP, data) - - -def train_group(hass, group): - """Train a person group.""" - data = {ATTR_GROUP: group} - hass.services.call(DOMAIN, SERVICE_TRAIN_GROUP, data) - - -def create_person(hass, group, name): - """Create a person in a group.""" - data = {ATTR_GROUP: group, ATTR_NAME: name} - hass.services.call(DOMAIN, SERVICE_CREATE_PERSON, data) - - -def delete_person(hass, group, name): - """Delete a person in a group.""" - data = {ATTR_GROUP: group, ATTR_NAME: name} - hass.services.call(DOMAIN, SERVICE_DELETE_PERSON, data) - - -def face_person(hass, group, person, camera_entity): - """Add a new face picture to a person.""" - data = {ATTR_GROUP: group, ATTR_PERSON: person, - ATTR_CAMERA_ENTITY: camera_entity} - hass.services.call(DOMAIN, SERVICE_FACE_PERSON, data) - - async def async_setup(hass, config): """Set up Microsoft Face.""" entities = {} diff --git a/tests/components/test_ffmpeg.py b/tests/components/test_ffmpeg.py index 76b1300774b..774bb471f57 100644 --- a/tests/components/test_ffmpeg.py +++ b/tests/components/test_ffmpeg.py @@ -3,12 +3,46 @@ import asyncio from unittest.mock import patch, MagicMock import homeassistant.components.ffmpeg as ffmpeg +from homeassistant.components.ffmpeg import ( + DOMAIN, SERVICE_RESTART, SERVICE_START, SERVICE_STOP) +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.core import callback from homeassistant.setup import setup_component, async_setup_component from tests.common import ( get_test_home_assistant, assert_setup_component, mock_coro) +@callback +def async_start(hass, entity_id=None): + """Start a FFmpeg process on entity. + + This is a legacy helper method. Do not use it for new tests. + """ + data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} + hass.async_add_job(hass.services.async_call(DOMAIN, SERVICE_START, data)) + + +@callback +def async_stop(hass, entity_id=None): + """Stop a FFmpeg process on entity. + + This is a legacy helper method. Do not use it for new tests. + """ + data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} + hass.async_add_job(hass.services.async_call(DOMAIN, SERVICE_STOP, data)) + + +@callback +def async_restart(hass, entity_id=None): + """Restart a FFmpeg process on entity. + + This is a legacy helper method. Do not use it for new tests. + """ + data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} + hass.async_add_job(hass.services.async_call(DOMAIN, SERVICE_RESTART, data)) + + class MockFFmpegDev(ffmpeg.FFmpegBase): """FFmpeg device mock.""" @@ -106,7 +140,7 @@ def test_setup_component_test_service_start(hass): ffmpeg_dev = MockFFmpegDev(hass, False) yield from ffmpeg_dev.async_added_to_hass() - ffmpeg.async_start(hass) + async_start(hass) yield from hass.async_block_till_done() assert ffmpeg_dev.called_start @@ -122,7 +156,7 @@ def test_setup_component_test_service_stop(hass): ffmpeg_dev = MockFFmpegDev(hass, False) yield from ffmpeg_dev.async_added_to_hass() - ffmpeg.async_stop(hass) + async_stop(hass) yield from hass.async_block_till_done() assert ffmpeg_dev.called_stop @@ -138,7 +172,7 @@ def test_setup_component_test_service_restart(hass): ffmpeg_dev = MockFFmpegDev(hass, False) yield from ffmpeg_dev.async_added_to_hass() - ffmpeg.async_restart(hass) + async_restart(hass) yield from hass.async_block_till_done() assert ffmpeg_dev.called_stop @@ -155,7 +189,7 @@ def test_setup_component_test_service_start_with_entity(hass): ffmpeg_dev = MockFFmpegDev(hass, False) yield from ffmpeg_dev.async_added_to_hass() - ffmpeg.async_start(hass, 'test.ffmpeg_device') + async_start(hass, 'test.ffmpeg_device') yield from hass.async_block_till_done() assert ffmpeg_dev.called_start diff --git a/tests/components/test_init.py b/tests/components/test_init.py index 355f3dc0e96..68396f5abcb 100644 --- a/tests/components/test_init.py +++ b/tests/components/test_init.py @@ -8,8 +8,12 @@ import yaml import homeassistant.core as ha from homeassistant import config from homeassistant.const import ( - STATE_ON, STATE_OFF, SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE) + ATTR_ENTITY_ID, STATE_ON, STATE_OFF, SERVICE_HOMEASSISTANT_RESTART, + SERVICE_HOMEASSISTANT_STOP, SERVICE_TURN_ON, SERVICE_TURN_OFF, + SERVICE_TOGGLE) import homeassistant.components as comps +from homeassistant.components import ( + SERVICE_CHECK_CONFIG, SERVICE_RELOAD_CORE_CONFIG) import homeassistant.helpers.intent as intent from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity @@ -20,6 +24,71 @@ from tests.common import ( async_mock_service) +def turn_on(hass, entity_id=None, **service_data): + """Turn specified entity on if possible. + + This is a legacy helper method. Do not use it for new tests. + """ + if entity_id is not None: + service_data[ATTR_ENTITY_ID] = entity_id + + hass.services.call(ha.DOMAIN, SERVICE_TURN_ON, service_data) + + +def turn_off(hass, entity_id=None, **service_data): + """Turn specified entity off. + + This is a legacy helper method. Do not use it for new tests. + """ + if entity_id is not None: + service_data[ATTR_ENTITY_ID] = entity_id + + hass.services.call(ha.DOMAIN, SERVICE_TURN_OFF, service_data) + + +def toggle(hass, entity_id=None, **service_data): + """Toggle specified entity. + + This is a legacy helper method. Do not use it for new tests. + """ + if entity_id is not None: + service_data[ATTR_ENTITY_ID] = entity_id + + hass.services.call(ha.DOMAIN, SERVICE_TOGGLE, service_data) + + +def stop(hass): + """Stop Home Assistant. + + This is a legacy helper method. Do not use it for new tests. + """ + hass.services.call(ha.DOMAIN, SERVICE_HOMEASSISTANT_STOP) + + +def restart(hass): + """Stop Home Assistant. + + This is a legacy helper method. Do not use it for new tests. + """ + hass.services.call(ha.DOMAIN, SERVICE_HOMEASSISTANT_RESTART) + + +def check_config(hass): + """Check the config files. + + This is a legacy helper method. Do not use it for new tests. + """ + hass.services.call(ha.DOMAIN, SERVICE_CHECK_CONFIG) + + +def reload_core_config(hass): + """Reload the core config. + + This is a legacy helper method. Do not use it for new tests. + """ + hass.services.call(ha.DOMAIN, SERVICE_RELOAD_CORE_CONFIG) + + class TestComponentsCore(unittest.TestCase): """Test homeassistant.components module.""" @@ -49,28 +118,28 @@ class TestComponentsCore(unittest.TestCase): def test_turn_on_without_entities(self): """Test turn_on method without entities.""" calls = mock_service(self.hass, 'light', SERVICE_TURN_ON) - comps.turn_on(self.hass) + turn_on(self.hass) self.hass.block_till_done() self.assertEqual(0, len(calls)) def test_turn_on(self): """Test turn_on method.""" calls = mock_service(self.hass, 'light', SERVICE_TURN_ON) - comps.turn_on(self.hass, 'light.Ceiling') + turn_on(self.hass, 'light.Ceiling') self.hass.block_till_done() self.assertEqual(1, len(calls)) def test_turn_off(self): """Test turn_off method.""" calls = mock_service(self.hass, 'light', SERVICE_TURN_OFF) - comps.turn_off(self.hass, 'light.Bowl') + turn_off(self.hass, 'light.Bowl') self.hass.block_till_done() self.assertEqual(1, len(calls)) def test_toggle(self): """Test toggle method.""" calls = mock_service(self.hass, 'light', SERVICE_TOGGLE) - comps.toggle(self.hass, 'light.Bowl') + toggle(self.hass, 'light.Bowl') self.hass.block_till_done() self.assertEqual(1, len(calls)) @@ -102,7 +171,7 @@ class TestComponentsCore(unittest.TestCase): }) } with patch_yaml_files(files, True): - comps.reload_core_config(self.hass) + reload_core_config(self.hass) self.hass.block_till_done() assert self.hass.config.latitude == 10 @@ -125,7 +194,7 @@ class TestComponentsCore(unittest.TestCase): config.YAML_CONFIG_FILE: yaml.dump(['invalid', 'config']) } with patch_yaml_files(files, True): - comps.reload_core_config(self.hass) + reload_core_config(self.hass) self.hass.block_till_done() assert mock_error.called @@ -135,7 +204,7 @@ class TestComponentsCore(unittest.TestCase): return_value=mock_coro()) def test_stop_homeassistant(self, mock_stop): """Test stop service.""" - comps.stop(self.hass) + stop(self.hass) self.hass.block_till_done() assert mock_stop.called @@ -145,7 +214,7 @@ class TestComponentsCore(unittest.TestCase): return_value=mock_coro()) def test_restart_homeassistant(self, mock_check, mock_restart): """Test stop service.""" - comps.restart(self.hass) + restart(self.hass) self.hass.block_till_done() assert mock_restart.called assert mock_check.called @@ -156,7 +225,7 @@ class TestComponentsCore(unittest.TestCase): side_effect=HomeAssistantError("Test error")) def test_restart_homeassistant_wrong_conf(self, mock_check, mock_restart): """Test stop service.""" - comps.restart(self.hass) + restart(self.hass) self.hass.block_till_done() assert mock_check.called assert not mock_restart.called @@ -167,7 +236,7 @@ class TestComponentsCore(unittest.TestCase): return_value=mock_coro()) def test_check_config(self, mock_check, mock_stop): """Test stop service.""" - comps.check_config(self.hass) + check_config(self.hass) self.hass.block_till_done() assert mock_check.called assert not mock_stop.called diff --git a/tests/components/test_microsoft_face.py b/tests/components/test_microsoft_face.py index 601d5e7ebcc..6c9c58da7df 100644 --- a/tests/components/test_microsoft_face.py +++ b/tests/components/test_microsoft_face.py @@ -3,12 +3,72 @@ import asyncio from unittest.mock import patch from homeassistant.components import camera, microsoft_face as mf +from homeassistant.components.microsoft_face import ( + ATTR_CAMERA_ENTITY, ATTR_GROUP, ATTR_PERSON, DOMAIN, SERVICE_CREATE_GROUP, + SERVICE_CREATE_PERSON, SERVICE_DELETE_GROUP, SERVICE_DELETE_PERSON, + SERVICE_FACE_PERSON, SERVICE_TRAIN_GROUP) +from homeassistant.const import ATTR_NAME from homeassistant.setup import setup_component from tests.common import ( get_test_home_assistant, assert_setup_component, mock_coro, load_fixture) +def create_group(hass, name): + """Create a new person group. + + This is a legacy helper method. Do not use it for new tests. + """ + data = {ATTR_NAME: name} + hass.services.call(DOMAIN, SERVICE_CREATE_GROUP, data) + + +def delete_group(hass, name): + """Delete a person group. + + This is a legacy helper method. Do not use it for new tests. + """ + data = {ATTR_NAME: name} + hass.services.call(DOMAIN, SERVICE_DELETE_GROUP, data) + + +def train_group(hass, group): + """Train a person group. + + This is a legacy helper method. Do not use it for new tests. + """ + data = {ATTR_GROUP: group} + hass.services.call(DOMAIN, SERVICE_TRAIN_GROUP, data) + + +def create_person(hass, group, name): + """Create a person in a group. + + This is a legacy helper method. Do not use it for new tests. + """ + data = {ATTR_GROUP: group, ATTR_NAME: name} + hass.services.call(DOMAIN, SERVICE_CREATE_PERSON, data) + + +def delete_person(hass, group, name): + """Delete a person in a group. + + This is a legacy helper method. Do not use it for new tests. + """ + data = {ATTR_GROUP: group, ATTR_NAME: name} + hass.services.call(DOMAIN, SERVICE_DELETE_PERSON, data) + + +def face_person(hass, group, person, camera_entity): + """Add a new face picture to a person. + + This is a legacy helper method. Do not use it for new tests. + """ + data = {ATTR_GROUP: group, ATTR_PERSON: person, + ATTR_CAMERA_ENTITY: camera_entity} + hass.services.call(DOMAIN, SERVICE_FACE_PERSON, data) + + class TestMicrosoftFaceSetup: """Test the microsoft face component.""" @@ -108,14 +168,14 @@ class TestMicrosoftFaceSetup: with assert_setup_component(3, mf.DOMAIN): setup_component(self.hass, mf.DOMAIN, self.config) - mf.create_group(self.hass, 'Service Group') + create_group(self.hass, 'Service Group') self.hass.block_till_done() entity = self.hass.states.get('microsoft_face.service_group') assert entity is not None assert len(aioclient_mock.mock_calls) == 1 - mf.delete_group(self.hass, 'Service Group') + delete_group(self.hass, 'Service Group') self.hass.block_till_done() entity = self.hass.states.get('microsoft_face.service_group') @@ -153,7 +213,7 @@ class TestMicrosoftFaceSetup: status=200, text="{}" ) - mf.create_person(self.hass, 'test group1', 'Hans') + create_person(self.hass, 'test group1', 'Hans') self.hass.block_till_done() entity_group1 = self.hass.states.get('microsoft_face.test_group1') @@ -163,7 +223,7 @@ class TestMicrosoftFaceSetup: assert entity_group1.attributes['Hans'] == \ '25985303-c537-4467-b41d-bdb45cd95ca1' - mf.delete_person(self.hass, 'test group1', 'Hans') + delete_person(self.hass, 'test group1', 'Hans') self.hass.block_till_done() entity_group1 = self.hass.states.get('microsoft_face.test_group1') @@ -184,7 +244,7 @@ class TestMicrosoftFaceSetup: status=200, text="{}" ) - mf.train_group(self.hass, 'Service Group') + train_group(self.hass, 'Service Group') self.hass.block_till_done() assert len(aioclient_mock.mock_calls) == 1 @@ -219,7 +279,7 @@ class TestMicrosoftFaceSetup: status=200, text="{}" ) - mf.face_person( + face_person( self.hass, 'test_group2', 'David', 'camera.demo_camera') self.hass.block_till_done() @@ -238,7 +298,7 @@ class TestMicrosoftFaceSetup: with assert_setup_component(3, mf.DOMAIN): setup_component(self.hass, mf.DOMAIN, self.config) - mf.create_group(self.hass, 'Service Group') + create_group(self.hass, 'Service Group') self.hass.block_till_done() entity = self.hass.states.get('microsoft_face.service_group') @@ -257,7 +317,7 @@ class TestMicrosoftFaceSetup: with assert_setup_component(3, mf.DOMAIN): setup_component(self.hass, mf.DOMAIN, self.config) - mf.create_group(self.hass, 'Service Group') + create_group(self.hass, 'Service Group') self.hass.block_till_done() entity = self.hass.states.get('microsoft_face.service_group') From 0954eefa9f865ee45b8700ed3345f70884867586 Mon Sep 17 00:00:00 2001 From: Charles Garwood Date: Tue, 2 Oct 2018 04:24:44 -0400 Subject: [PATCH 154/247] MJPEG Camera Log Filter Fixes (#17050) * Move filter to setup platform * pylint --- homeassistant/components/camera/mjpeg.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/camera/mjpeg.py b/homeassistant/components/camera/mjpeg.py index f758eb11e9d..1df06b546cd 100644 --- a/homeassistant/components/camera/mjpeg.py +++ b/homeassistant/components/camera/mjpeg.py @@ -44,6 +44,14 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up a MJPEG IP Camera.""" + # Filter header errors from urllib3 due to a urllib3 bug + urllib3_logger = logging.getLogger("urllib3.connectionpool") + if not any(isinstance(x, NoHeaderErrorFilter) + for x in urllib3_logger.filters): + urllib3_logger.addFilter( + NoHeaderErrorFilter() + ) + if discovery_info: config = PLATFORM_SCHEMA(discovery_info) async_add_entities([MjpegCamera(config)]) @@ -74,10 +82,6 @@ class MjpegCamera(Camera): self._mjpeg_url = device_info[CONF_MJPEG_URL] self._still_image_url = device_info.get(CONF_STILL_IMAGE_URL) - logging.getLogger("urllib3.connectionpool").addFilter( - NoHeaderErrorFilter() - ) - self._auth = None if self._username and self._password: if self._authentication == HTTP_BASIC_AUTHENTICATION: From 71e3047f5c544b42ffe70ec34c77b4236daea3c2 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Tue, 2 Oct 2018 02:34:12 -0600 Subject: [PATCH 155/247] OpenUV: Fixed issue with missing protection window data (#17051) --- homeassistant/components/binary_sensor/openuv.py | 6 +++++- homeassistant/components/openuv/__init__.py | 11 +++++++++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/binary_sensor/openuv.py b/homeassistant/components/binary_sensor/openuv.py index c7c27d73ee4..bd6e4d1d5dc 100644 --- a/homeassistant/components/binary_sensor/openuv.py +++ b/homeassistant/components/binary_sensor/openuv.py @@ -93,7 +93,11 @@ class OpenUvBinarySensor(OpenUvEntity, BinarySensorDevice): async def async_update(self): """Update the state.""" - data = self.openuv.data[DATA_PROTECTION_WINDOW]['result'] + data = self.openuv.data[DATA_PROTECTION_WINDOW] + + if not data: + return + if self._sensor_type == TYPE_PROTECTION_WINDOW: self._state = parse_datetime( data['from_time']) <= utcnow() <= parse_datetime( diff --git a/homeassistant/components/openuv/__init__.py b/homeassistant/components/openuv/__init__.py index 35ab16b4d1f..8485e1e3201 100644 --- a/homeassistant/components/openuv/__init__.py +++ b/homeassistant/components/openuv/__init__.py @@ -212,8 +212,15 @@ class OpenUV: async def async_update(self): """Update sensor/binary sensor data.""" if TYPE_PROTECTION_WINDOW in self.binary_sensor_conditions: - data = await self.client.uv_protection_window() - self.data[DATA_PROTECTION_WINDOW] = data + resp = await self.client.uv_protection_window() + data = resp['result'] + + if data.get('from_time') and data.get('to_time'): + self.data[DATA_PROTECTION_WINDOW] = data + else: + _LOGGER.error( + 'No valid protection window data for this location') + self.data[DATA_PROTECTION_WINDOW] = {} if any(c in self.sensor_conditions for c in SENSORS): data = await self.client.uv_index() From 37706c273153c03553a3d9f44dd19748d70bd531 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 2 Oct 2018 10:35:00 +0200 Subject: [PATCH 156/247] Lint --- homeassistant/__main__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/__main__.py b/homeassistant/__main__.py index 973080c2e61..a424716f0aa 100644 --- a/homeassistant/__main__.py +++ b/homeassistant/__main__.py @@ -28,6 +28,7 @@ def set_loop() -> None: if sys.platform == 'win32': if hasattr(asyncio, 'WindowsProactorEventLoopPolicy'): + # pylint: disable=no-member policy = asyncio.WindowsProactorEventLoopPolicy() else: class ProactorPolicy(BaseDefaultEventLoopPolicy): From 7f0a50ce31befab10e8ba1892cc30b1334f22fc2 Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Tue, 2 Oct 2018 11:03:09 +0200 Subject: [PATCH 157/247] async_create_task (#17059) * async_create_task * Update google.py --- homeassistant/components/apple_tv.py | 2 +- homeassistant/components/camera/__init__.py | 2 +- homeassistant/components/cloud/iot.py | 4 ++-- homeassistant/components/config/__init__.py | 4 ++-- homeassistant/components/config/auth.py | 6 +++--- homeassistant/components/config/device_registry.py | 2 +- homeassistant/components/device_tracker/__init__.py | 8 ++++---- homeassistant/components/device_tracker/automatic.py | 6 +++--- homeassistant/components/device_tracker/gpslogger.py | 2 +- homeassistant/components/device_tracker/meraki.py | 2 +- homeassistant/components/device_tracker/mqtt.py | 2 +- homeassistant/components/device_tracker/mqtt_json.py | 2 +- homeassistant/components/discovery.py | 4 ++-- homeassistant/components/emulated_hue/hue_api.py | 4 ++-- homeassistant/components/homematicip_cloud/hap.py | 6 +++--- homeassistant/components/hue/__init__.py | 2 +- homeassistant/components/hue/bridge.py | 2 +- homeassistant/components/intent_script.py | 2 +- homeassistant/components/light/lifx.py | 8 ++++---- homeassistant/components/light/template.py | 2 +- homeassistant/components/light/tradfri.py | 4 ++-- homeassistant/components/media_extractor.py | 2 +- homeassistant/components/media_player/anthemav.py | 2 +- homeassistant/components/media_player/bluesound.py | 4 ++-- homeassistant/components/media_player/cast.py | 4 ++-- homeassistant/components/media_player/kodi.py | 6 +++--- homeassistant/components/mqtt/__init__.py | 5 +++-- homeassistant/components/mysensors/__init__.py | 2 +- homeassistant/components/mysensors/gateway.py | 4 ++-- homeassistant/components/nest/__init__.py | 2 +- .../components/persistent_notification/__init__.py | 3 ++- homeassistant/components/rflink.py | 2 +- homeassistant/components/sensor/dsmr.py | 2 +- homeassistant/components/sensor/tradfri.py | 4 ++-- homeassistant/components/sisyphus.py | 4 ++-- homeassistant/components/switch/netio.py | 2 +- homeassistant/components/tts/__init__.py | 4 ++-- homeassistant/components/tuya.py | 2 +- homeassistant/components/zha/__init__.py | 8 +++++--- homeassistant/components/zone/__init__.py | 6 +++--- tests/components/hue/test_bridge.py | 4 ++-- 41 files changed, 76 insertions(+), 72 deletions(-) diff --git a/homeassistant/components/apple_tv.py b/homeassistant/components/apple_tv.py index 96e31e6bbf1..012e71a08a7 100644 --- a/homeassistant/components/apple_tv.py +++ b/homeassistant/components/apple_tv.py @@ -252,4 +252,4 @@ class AppleTVPowerManager: self.atv.push_updater.start() for listener in self.listeners: - self.hass.async_add_job(listener.async_update_ha_state()) + self.hass.async_create_task(listener.async_update_ha_state()) diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index 2cf23e0d60c..5897d972b31 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -183,7 +183,7 @@ async def async_setup(hass, config): """Update tokens of the entities.""" for entity in component.entities: entity.async_update_token() - hass.async_add_job(entity.async_update_ha_state()) + hass.async_create_task(entity.async_update_ha_state()) hass.helpers.event.async_track_time_interval( update_tokens, TOKEN_CHANGE_INTERVAL) diff --git a/homeassistant/components/cloud/iot.py b/homeassistant/components/cloud/iot.py index fd525ed33a8..fe89c263488 100644 --- a/homeassistant/components/cloud/iot.py +++ b/homeassistant/components/cloud/iot.py @@ -79,7 +79,7 @@ class CloudIoT: try: # Sleep 2^tries seconds between retries - self.retry_task = hass.async_add_job(asyncio.sleep( + self.retry_task = hass.async_create_task(asyncio.sleep( 2**min(9, self.tries), loop=hass.loop)) yield from self.retry_task self.retry_task = None @@ -106,7 +106,7 @@ class CloudIoT: 'cloud_subscription_expired') # Don't await it because it will cancel this task - hass.async_add_job(self.cloud.logout()) + hass.async_create_task(self.cloud.logout()) return except auth_api.CloudError as err: _LOGGER.warning("Unable to refresh token: %s", err) diff --git a/homeassistant/components/config/__init__.py b/homeassistant/components/config/__init__.py index df0e2f13ac1..f2cfff1f342 100644 --- a/homeassistant/components/config/__init__.py +++ b/homeassistant/components/config/__init__.py @@ -52,7 +52,7 @@ async def async_setup(hass, config): """Respond to components being loaded.""" panel_name = event.data.get(ATTR_COMPONENT) if panel_name in ON_DEMAND: - hass.async_add_job(setup_panel(panel_name)) + hass.async_create_task(setup_panel(panel_name)) hass.bus.async_listen(EVENT_COMPONENT_LOADED, component_loaded) @@ -136,7 +136,7 @@ class BaseEditConfigView(HomeAssistantView): await hass.async_add_job(_write, path, current) if self.post_write_hook is not None: - hass.async_add_job(self.post_write_hook(hass)) + hass.async_create_task(self.post_write_hook(hass)) return self.json({ 'result': 'ok', diff --git a/homeassistant/components/config/auth.py b/homeassistant/components/config/auth.py index f2af6589f11..6da0fe61d96 100644 --- a/homeassistant/components/config/auth.py +++ b/homeassistant/components/config/auth.py @@ -52,7 +52,7 @@ def websocket_list(hass, connection, msg): connection.send_message( websocket_api.result_message(msg['id'], result)) - hass.async_add_job(send_users()) + hass.async_create_task(send_users()) @callback @@ -79,7 +79,7 @@ def websocket_delete(hass, connection, msg): connection.send_message( websocket_api.result_message(msg['id'])) - hass.async_add_job(delete_user()) + hass.async_create_task(delete_user()) @callback @@ -95,7 +95,7 @@ def websocket_create(hass, connection, msg): 'user': _user_info(user) })) - hass.async_add_job(create_user()) + hass.async_create_task(create_user()) def _user_info(user): diff --git a/homeassistant/components/config/device_registry.py b/homeassistant/components/config/device_registry.py index 54396f8956c..acffd516d21 100644 --- a/homeassistant/components/config/device_registry.py +++ b/homeassistant/components/config/device_registry.py @@ -44,4 +44,4 @@ def websocket_list_devices(hass, connection, msg): } for entry in registry.devices.values()] )) - hass.async_add_job(retrieve_entities()) + hass.async_create_task(retrieve_entities()) diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index b9cc72e5a5a..a9cc122a6f9 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -371,7 +371,7 @@ class DeviceTracker: for device in self.devices.values(): if (device.track and device.last_update_home) and \ device.stale(now): - self.hass.async_add_job(device.async_update_ha_state(True)) + self.hass.async_create_task(device.async_update_ha_state(True)) async def async_setup_tracked_device(self): """Set up all not exists tracked devices. @@ -386,7 +386,7 @@ class DeviceTracker: tasks = [] for device in self.devices.values(): if device.track and not device.last_seen: - tasks.append(self.hass.async_add_job( + tasks.append(self.hass.async_create_task( async_init_single_device(device))) if tasks: @@ -718,10 +718,10 @@ def async_setup_scanner_platform(hass: HomeAssistantType, config: ConfigType, zone_home.attributes[ATTR_LONGITUDE]] kwargs['gps_accuracy'] = 0 - hass.async_add_job(async_see_device(**kwargs)) + hass.async_create_task(async_see_device(**kwargs)) async_track_time_interval(hass, async_device_tracker_scan, interval) - hass.async_add_job(async_device_tracker_scan(None)) + hass.async_create_task(async_device_tracker_scan(None)) def update_config(path: str, dev_id: str, device: Device): diff --git a/homeassistant/components/device_tracker/automatic.py b/homeassistant/components/device_tracker/automatic.py index 4fcc550d7db..9f20eb6d493 100644 --- a/homeassistant/components/device_tracker/automatic.py +++ b/homeassistant/components/device_tracker/automatic.py @@ -113,7 +113,7 @@ def async_setup_scanner(hass, config, async_see, discovery_info=None): # Load the initial vehicle data vehicles = yield from session.get_vehicles() for vehicle in vehicles: - hass.async_add_job(data.load_vehicle(vehicle)) + hass.async_create_task(data.load_vehicle(vehicle)) # Create a task instead of adding a tracking job, since this task will # run until the websocket connection is closed. @@ -188,7 +188,7 @@ class AutomaticAuthCallbackView(HomeAssistantView): code = params['code'] state = params['state'] initialize_callback = hass.data[DATA_CONFIGURING][state] - hass.async_add_job(initialize_callback(code, state)) + hass.async_create_task(initialize_callback(code, state)) return response @@ -209,7 +209,7 @@ class AutomaticData: self.ws_close_requested = False self.client.on_app_event( - lambda name, event: self.hass.async_add_job( + lambda name, event: self.hass.async_create_task( self.handle_event(name, event))) hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.ws_close()) diff --git a/homeassistant/components/device_tracker/gpslogger.py b/homeassistant/components/device_tracker/gpslogger.py index 6336ba51d23..f39684aa834 100644 --- a/homeassistant/components/device_tracker/gpslogger.py +++ b/homeassistant/components/device_tracker/gpslogger.py @@ -98,7 +98,7 @@ class GPSLoggerView(HomeAssistantView): if 'activity' in data: attrs['activity'] = data['activity'] - hass.async_add_job(self.async_see( + hass.async_create_task(self.async_see( dev_id=device, gps=gps_location, battery=battery, gps_accuracy=accuracy, diff --git a/homeassistant/components/device_tracker/meraki.py b/homeassistant/components/device_tracker/meraki.py index 416f202ab98..d12aff1127a 100644 --- a/homeassistant/components/device_tracker/meraki.py +++ b/homeassistant/components/device_tracker/meraki.py @@ -121,7 +121,7 @@ class MerakiView(HomeAssistantView): attrs['seenTime'] = i['seenTime'] if i.get('ssid', False): attrs['ssid'] = i['ssid'] - hass.async_add_job(self.async_see( + hass.async_create_task(self.async_see( gps=gps_location, mac=mac, source_type=SOURCE_TYPE_ROUTER, diff --git a/homeassistant/components/device_tracker/mqtt.py b/homeassistant/components/device_tracker/mqtt.py index 3623abf31c3..06bd6d771a4 100644 --- a/homeassistant/components/device_tracker/mqtt.py +++ b/homeassistant/components/device_tracker/mqtt.py @@ -33,7 +33,7 @@ async def async_setup_scanner(hass, config, async_see, discovery_info=None): @callback def async_message_received(topic, payload, qos, dev_id=dev_id): """Handle received MQTT message.""" - hass.async_add_job( + hass.async_create_task( async_see(dev_id=dev_id, location_name=payload)) await mqtt.async_subscribe( diff --git a/homeassistant/components/device_tracker/mqtt_json.py b/homeassistant/components/device_tracker/mqtt_json.py index a95180f2eb2..3a820d189f4 100644 --- a/homeassistant/components/device_tracker/mqtt_json.py +++ b/homeassistant/components/device_tracker/mqtt_json.py @@ -55,7 +55,7 @@ async def async_setup_scanner(hass, config, async_see, discovery_info=None): return kwargs = _parse_see_args(dev_id, data) - hass.async_add_job(async_see(**kwargs)) + hass.async_create_task(async_see(**kwargs)) await mqtt.async_subscribe( hass, topic, async_message_received, qos) diff --git a/homeassistant/components/discovery.py b/homeassistant/components/discovery.py index 91f9dea704b..9377db52609 100644 --- a/homeassistant/components/discovery.py +++ b/homeassistant/components/discovery.py @@ -168,7 +168,7 @@ async def async_setup(hass, config): results = await hass.async_add_job(_discover, netdisco) for result in results: - hass.async_add_job(new_service_found(*result)) + hass.async_create_task(new_service_found(*result)) async_track_point_in_utc_time(hass, scan_devices, dt_util.utcnow() + SCAN_INTERVAL) @@ -180,7 +180,7 @@ async def async_setup(hass, config): # discovery local services if 'HASSIO' in os.environ: - hass.async_add_job(new_service_found(SERVICE_HASSIO, {})) + hass.async_create_task(new_service_found(SERVICE_HASSIO, {})) hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, schedule_first) diff --git a/homeassistant/components/emulated_hue/hue_api.py b/homeassistant/components/emulated_hue/hue_api.py index fa24b656d7a..c6fa622513b 100644 --- a/homeassistant/components/emulated_hue/hue_api.py +++ b/homeassistant/components/emulated_hue/hue_api.py @@ -254,11 +254,11 @@ class HueOneLightChangeView(HomeAssistantView): # Separate call to turn on needed if turn_on_needed: - hass.async_add_job(hass.services.async_call( + hass.async_create_task(hass.services.async_call( core.DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: entity_id}, blocking=True)) - hass.async_add_job(hass.services.async_call( + hass.async_create_task(hass.services.async_call( domain, service, data, blocking=True)) json_response = \ diff --git a/homeassistant/components/homematicip_cloud/hap.py b/homeassistant/components/homematicip_cloud/hap.py index 6fddc7c001e..d79e7c1ee14 100644 --- a/homeassistant/components/homematicip_cloud/hap.py +++ b/homeassistant/components/homematicip_cloud/hap.py @@ -142,7 +142,7 @@ class HomematicipHAP: # Explicitly getting an update as device states might have # changed during access point disconnect.""" - job = self.hass.async_add_job(self.get_state()) + job = self.hass.async_create_task(self.get_state()) job.add_done_callback(self.get_state_finished) async def get_state(self): @@ -161,7 +161,7 @@ class HomematicipHAP: # so reconnect loop is taking over. _LOGGER.error( "Updating state after HMIP access point reconnect failed") - self.hass.async_add_job(self.home.disable_events()) + self.hass.async_create_task(self.home.disable_events()) def set_all_to_unavailable(self): """Set all devices to unavailable and tell Home Assistant.""" @@ -212,7 +212,7 @@ class HomematicipHAP: "Retrying in %d seconds", self.config_entry.data.get(HMIPC_HAPID), retry_delay) try: - self._retry_task = self.hass.async_add_job(asyncio.sleep( + self._retry_task = self.hass.async_create_task(asyncio.sleep( retry_delay, loop=self.hass.loop)) await self._retry_task except asyncio.CancelledError: diff --git a/homeassistant/components/hue/__init__.py b/homeassistant/components/hue/__init__.py index 7a781c99f53..8431e5dbe3d 100644 --- a/homeassistant/components/hue/__init__.py +++ b/homeassistant/components/hue/__init__.py @@ -108,7 +108,7 @@ async def async_setup(hass, config): # this component we'll have to use hass.async_add_job to avoid a # deadlock: creating a config entry will set up the component but the # setup would block till the entry is created! - hass.async_add_job(hass.config_entries.flow.async_init( + hass.async_create_task(hass.config_entries.flow.async_init( DOMAIN, context={'source': config_entries.SOURCE_IMPORT}, data={ 'host': bridge_conf[CONF_HOST], diff --git a/homeassistant/components/hue/bridge.py b/homeassistant/components/hue/bridge.py index 874c18aaa7e..93241622f0b 100644 --- a/homeassistant/components/hue/bridge.py +++ b/homeassistant/components/hue/bridge.py @@ -50,7 +50,7 @@ class HueBridge: # We are going to fail the config entry setup and initiate a new # linking procedure. When linking succeeds, it will remove the # old config entry. - hass.async_add_job(hass.config_entries.flow.async_init( + hass.async_create_task(hass.config_entries.flow.async_init( DOMAIN, context={'source': config_entries.SOURCE_IMPORT}, data={ 'host': host, diff --git a/homeassistant/components/intent_script.py b/homeassistant/components/intent_script.py index 0c47b8880ba..9c63141e496 100644 --- a/homeassistant/components/intent_script.py +++ b/homeassistant/components/intent_script.py @@ -78,7 +78,7 @@ class ScriptIntentHandler(intent.IntentHandler): if action is not None: if is_async_action: - intent_obj.hass.async_add_job(action.async_run(slots)) + intent_obj.hass.async_create_task(action.async_run(slots)) else: await action.async_run(slots) diff --git a/homeassistant/components/light/lifx.py b/homeassistant/components/light/lifx.py index 21a501fa6ea..17b9f104f68 100644 --- a/homeassistant/components/light/lifx.py +++ b/homeassistant/components/light/lifx.py @@ -300,7 +300,7 @@ class LIFXManager: @callback def register(self, bulb): """Handle aiolifx detected bulb.""" - self.hass.async_add_job(self.register_new_bulb(bulb)) + self.hass.async_create_task(self.register_new_bulb(bulb)) async def register_new_bulb(self, bulb): """Handle newly detected bulb.""" @@ -344,7 +344,7 @@ class LIFXManager: entity = self.entities[bulb.mac_addr] _LOGGER.debug("%s unregister", entity.who) entity.registered = False - self.hass.async_add_job(entity.async_update_ha_state()) + self.hass.async_create_task(entity.async_update_ha_state()) class AwaitAioLIFX: @@ -484,12 +484,12 @@ class LIFXLight(Light): async def async_turn_on(self, **kwargs): """Turn the light on.""" kwargs[ATTR_POWER] = True - self.hass.async_add_job(self.set_state(**kwargs)) + self.hass.async_create_task(self.set_state(**kwargs)) async def async_turn_off(self, **kwargs): """Turn the light off.""" kwargs[ATTR_POWER] = False - self.hass.async_add_job(self.set_state(**kwargs)) + self.hass.async_create_task(self.set_state(**kwargs)) async def set_state(self, **kwargs): """Set a color on the light and turn it on/off.""" diff --git a/homeassistant/components/light/template.py b/homeassistant/components/light/template.py index f90147f7169..8aff85c6001 100644 --- a/homeassistant/components/light/template.py +++ b/homeassistant/components/light/template.py @@ -215,7 +215,7 @@ class LightTemplate(Light): optimistic_set = True if ATTR_BRIGHTNESS in kwargs and self._level_script: - self.hass.async_add_job(self._level_script.async_run( + self.hass.async_create_task(self._level_script.async_run( {"brightness": kwargs[ATTR_BRIGHTNESS]})) else: await self._on_script.async_run() diff --git a/homeassistant/components/light/tradfri.py b/homeassistant/components/light/tradfri.py index b62900b204c..a26a2eb828a 100644 --- a/homeassistant/components/light/tradfri.py +++ b/homeassistant/components/light/tradfri.py @@ -127,7 +127,7 @@ class TradfriGroup(Light): cmd = self._group.observe(callback=self._observe_update, err_callback=self._async_start_observe, duration=0) - self.hass.async_add_job(self._api(cmd)) + self.hass.async_create_task(self._api(cmd)) except PytradfriError as err: _LOGGER.warning("Observation failed, trying again", exc_info=err) self._async_start_observe() @@ -346,7 +346,7 @@ class TradfriLight(Light): cmd = self._light.observe(callback=self._observe_update, err_callback=self._async_start_observe, duration=0) - self.hass.async_add_job(self._api(cmd)) + self.hass.async_create_task(self._api(cmd)) except PytradfriError as err: _LOGGER.warning("Observation failed, trying again", exc_info=err) self._async_start_observe() diff --git a/homeassistant/components/media_extractor.py b/homeassistant/components/media_extractor.py index a7093579805..b3c02e5aee6 100644 --- a/homeassistant/components/media_extractor.py +++ b/homeassistant/components/media_extractor.py @@ -148,7 +148,7 @@ class MediaExtractor: if entity_id: data[ATTR_ENTITY_ID] = entity_id - self.hass.async_add_job( + self.hass.async_create_task( self.hass.services.async_call( MEDIA_PLAYER_DOMAIN, SERVICE_PLAY_MEDIA, data) ) diff --git a/homeassistant/components/media_player/anthemav.py b/homeassistant/components/media_player/anthemav.py index c1415f985e8..f1954a1d37e 100644 --- a/homeassistant/components/media_player/anthemav.py +++ b/homeassistant/components/media_player/anthemav.py @@ -49,7 +49,7 @@ async def async_setup_platform(hass, config, async_add_entities, def async_anthemav_update_callback(message): """Receive notification from transport that new data exists.""" _LOGGER.info("Received update callback from AVR: %s", message) - hass.async_add_job(device.async_update_ha_state()) + hass.async_create_task(device.async_update_ha_state()) avr = await anthemav.Connection.create( host=host, port=port, loop=hass.loop, diff --git a/homeassistant/components/media_player/bluesound.py b/homeassistant/components/media_player/bluesound.py index ab012402636..f4ed62b15cd 100644 --- a/homeassistant/components/media_player/bluesound.py +++ b/homeassistant/components/media_player/bluesound.py @@ -96,7 +96,7 @@ def _add_player(hass, async_add_entities, host, port=None, name=None): @callback def _init_player(event=None): """Start polling.""" - hass.async_add_job(player.async_init()) + hass.async_create_task(player.async_init()) @callback def _start_polling(event=None): @@ -272,7 +272,7 @@ class BluesoundPlayer(MediaPlayerDevice): def start_polling(self): """Start the polling task.""" - self._polling_task = self._hass.async_add_job( + self._polling_task = self._hass.async_create_task( self._start_poll_command()) def stop_polling(self): diff --git a/homeassistant/components/media_player/cast.py b/homeassistant/components/media_player/cast.py index 67d8ea0b419..d6515b9476d 100644 --- a/homeassistant/components/media_player/cast.py +++ b/homeassistant/components/media_player/cast.py @@ -343,7 +343,7 @@ class CastDevice(MediaPlayerDevice): # Discovered is not our device. return _LOGGER.debug("Discovered chromecast with same UUID: %s", discover) - self.hass.async_add_job(self.async_set_cast_info(discover)) + self.hass.async_create_task(self.async_set_cast_info(discover)) async def async_stop(event): """Disconnect socket on Home Assistant stop.""" @@ -352,7 +352,7 @@ class CastDevice(MediaPlayerDevice): async_dispatcher_connect(self.hass, SIGNAL_CAST_DISCOVERED, async_cast_discovered) self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_stop) - self.hass.async_add_job(self.async_set_cast_info(self._cast_info)) + self.hass.async_create_task(self.async_set_cast_info(self._cast_info)) async def async_will_remove_from_hass(self) -> None: """Disconnect Chromecast object when removed.""" diff --git a/homeassistant/components/media_player/kodi.py b/homeassistant/components/media_player/kodi.py index eb97c75c9ef..01d8069dc3b 100644 --- a/homeassistant/components/media_player/kodi.py +++ b/homeassistant/components/media_player/kodi.py @@ -322,7 +322,7 @@ class KodiDevice(MediaPlayerDevice): def on_hass_stop(event): """Close websocket connection when hass stops.""" - self.hass.async_add_job(self._ws_server.close()) + self.hass.async_create_task(self._ws_server.close()) self.hass.bus.async_listen_once( EVENT_HOMEASSISTANT_STOP, on_hass_stop) @@ -387,7 +387,7 @@ class KodiDevice(MediaPlayerDevice): self._properties = {} self._item = {} self._app_properties = {} - self.hass.async_add_job(self._ws_server.close()) + self.hass.async_create_task(self._ws_server.close()) async def _get_players(self): """Return the active player objects or None.""" @@ -456,7 +456,7 @@ class KodiDevice(MediaPlayerDevice): return if self._enable_websocket and not self._ws_server.connected: - self.hass.async_add_job(self.async_ws_connect()) + self.hass.async_create_task(self.async_ws_connect()) self._app_properties = \ await self.server.Application.GetProperties( diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index a9226117b5a..3e25563e9ba 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -254,7 +254,8 @@ def async_publish(hass: HomeAssistantType, topic: Any, payload, qos=None, """Publish message to an MQTT topic.""" data = _build_publish_data(topic, qos, retain) data[ATTR_PAYLOAD] = payload - hass.async_add_job(hass.services.async_call(DOMAIN, SERVICE_PUBLISH, data)) + hass.async_create_task( + hass.services.async_call(DOMAIN, SERVICE_PUBLISH, data)) @bind_hass @@ -670,7 +671,7 @@ class MQTT: if any(other.topic == topic for other in self.subscriptions): # Other subscriptions on topic remaining - don't unsubscribe. return - self.hass.async_add_job(self._async_unsubscribe(topic)) + self.hass.async_create_task(self._async_unsubscribe(topic)) return async_remove diff --git a/homeassistant/components/mysensors/__init__.py b/homeassistant/components/mysensors/__init__.py index 725494cd197..4f00247495a 100644 --- a/homeassistant/components/mysensors/__init__.py +++ b/homeassistant/components/mysensors/__init__.py @@ -111,7 +111,7 @@ async def async_setup(hass, config): hass.data[MYSENSORS_GATEWAYS] = gateways - hass.async_add_job(finish_setup(hass, gateways)) + hass.async_create_task(finish_setup(hass, gateways)) return True diff --git a/homeassistant/components/mysensors/gateway.py b/homeassistant/components/mysensors/gateway.py index 88725e67940..558e944f727 100644 --- a/homeassistant/components/mysensors/gateway.py +++ b/homeassistant/components/mysensors/gateway.py @@ -106,7 +106,7 @@ async def _get_gateway(hass, config, gateway_conf, persistence_file): """Call callback.""" sub_cb(*args) - hass.async_add_job( + hass.async_create_task( mqtt.async_subscribe(topic, internal_callback, qos)) gateway = mysensors.AsyncMQTTGateway( @@ -192,7 +192,7 @@ async def _gw_start(hass, gateway): @callback def gw_stop(event): """Trigger to stop the gateway.""" - hass.async_add_job(gateway.stop()) + hass.async_create_task(gateway.stop()) if not connect_task.done(): connect_task.cancel() diff --git a/homeassistant/components/nest/__init__.py b/homeassistant/components/nest/__init__.py index f609c774b12..c66abf1a8bd 100644 --- a/homeassistant/components/nest/__init__.py +++ b/homeassistant/components/nest/__init__.py @@ -105,7 +105,7 @@ async def async_setup(hass, config): filename = config.get(CONF_FILENAME, NEST_CONFIG_FILE) access_token_cache_file = hass.config.path(filename) - hass.async_add_job(hass.config_entries.flow.async_init( + hass.async_create_task(hass.config_entries.flow.async_init( DOMAIN, context={'source': config_entries.SOURCE_IMPORT}, data={ 'nest_conf_path': access_token_cache_file, diff --git a/homeassistant/components/persistent_notification/__init__.py b/homeassistant/components/persistent_notification/__init__.py index a0f5cdae24d..9c09ccced3d 100644 --- a/homeassistant/components/persistent_notification/__init__.py +++ b/homeassistant/components/persistent_notification/__init__.py @@ -95,7 +95,8 @@ def async_dismiss(hass: HomeAssistant, notification_id: str) -> None: """Remove a notification.""" data = {ATTR_NOTIFICATION_ID: notification_id} - hass.async_add_job(hass.services.async_call(DOMAIN, SERVICE_DISMISS, data)) + hass.async_create_task( + hass.services.async_call(DOMAIN, SERVICE_DISMISS, data)) async def async_setup(hass: HomeAssistant, config: dict) -> Awaitable[bool]: diff --git a/homeassistant/components/rflink.py b/homeassistant/components/rflink.py index f96ce3968c4..a8aeca273d6 100644 --- a/homeassistant/components/rflink.py +++ b/homeassistant/components/rflink.py @@ -443,7 +443,7 @@ class RflinkCommand(RflinkDevice): self._protocol.send_command, self._device_id, cmd)) if repetitions > 1: - self._repetition_task = self.hass.async_add_job( + self._repetition_task = self.hass.async_create_task( self._async_send_command(cmd, repetitions - 1)) diff --git a/homeassistant/components/sensor/dsmr.py b/homeassistant/components/sensor/dsmr.py index 02b2a5e8ff5..d54959813f8 100644 --- a/homeassistant/components/sensor/dsmr.py +++ b/homeassistant/components/sensor/dsmr.py @@ -167,7 +167,7 @@ async def async_setup_platform(hass, config, async_add_entities, # Make all device entities aware of new telegram for device in devices: device.telegram = telegram - hass.async_add_job(device.async_update_ha_state()) + hass.async_create_task(device.async_update_ha_state()) # Creates an asyncio.Protocol factory for reading DSMR telegrams from # serial and calls update_entities_telegram to update entities on arrival diff --git a/homeassistant/components/sensor/tradfri.py b/homeassistant/components/sensor/tradfri.py index 769857cb6df..45167874de2 100644 --- a/homeassistant/components/sensor/tradfri.py +++ b/homeassistant/components/sensor/tradfri.py @@ -93,7 +93,7 @@ class TradfriDevice(Entity): cmd = self._device.observe(callback=self._observe_update, err_callback=self._async_start_observe, duration=0) - self.hass.async_add_job(self._api(cmd)) + self.hass.async_create_task(self._api(cmd)) except PytradfriError as err: _LOGGER.warning("Observation failed, trying again", exc_info=err) self._async_start_observe() @@ -107,4 +107,4 @@ class TradfriDevice(Entity): """Receive new state data for this device.""" self._refresh(tradfri_device) - self.hass.async_add_job(self.async_update_ha_state()) + self.hass.async_create_task(self.async_update_ha_state()) diff --git a/homeassistant/components/sisyphus.py b/homeassistant/components/sisyphus.py index dc9f9cc4c25..f875e3a91c7 100644 --- a/homeassistant/components/sisyphus.py +++ b/homeassistant/components/sisyphus.py @@ -54,12 +54,12 @@ async def async_setup(hass, config): tables[name] = table _LOGGER.debug("Connected to %s at %s", name, host) - hass.async_add_job(async_load_platform( + hass.async_create_task(async_load_platform( hass, 'light', DOMAIN, { CONF_NAME: name, }, config )) - hass.async_add_job(async_load_platform( + hass.async_create_task(async_load_platform( hass, 'media_player', DOMAIN, { CONF_NAME: name, CONF_HOST: host, diff --git a/homeassistant/components/switch/netio.py b/homeassistant/components/switch/netio.py index 2ccb4501d73..fc6086f9897 100644 --- a/homeassistant/components/switch/netio.py +++ b/homeassistant/components/switch/netio.py @@ -117,7 +117,7 @@ class NetioApiView(HomeAssistantView): ndev.start_dates = start_dates for dev in DEVICES[host].entities: - hass.async_add_job(dev.async_update_ha_state()) + hass.async_create_task(dev.async_update_ha_state()) return self.json(True) diff --git a/homeassistant/components/tts/__init__.py b/homeassistant/components/tts/__init__.py index e5eef0e6135..86667782d09 100644 --- a/homeassistant/components/tts/__init__.py +++ b/homeassistant/components/tts/__init__.py @@ -299,7 +299,7 @@ class SpeechManager: # Is file store in file cache elif use_cache and key in self.file_cache: filename = self.file_cache[key] - self.hass.async_add_job(self.async_file_to_mem(key)) + self.hass.async_create_task(self.async_file_to_mem(key)) # Load speech from provider into memory else: filename = await self.async_get_tts_audio( @@ -331,7 +331,7 @@ class SpeechManager: self._async_store_to_memcache(key, filename, data) if cache: - self.hass.async_add_job( + self.hass.async_create_task( self.async_save_tts_audio(key, filename, data)) return filename diff --git a/homeassistant/components/tuya.py b/homeassistant/components/tuya.py index 33f34164b02..22a82dec8e2 100644 --- a/homeassistant/components/tuya.py +++ b/homeassistant/components/tuya.py @@ -159,7 +159,7 @@ class TuyaDevice(Entity): def _delete_callback(self, dev_id): """Remove this entity.""" if dev_id == self.object_id: - self.hass.async_add_job(self.async_remove()) + self.hass.async_create_task(self.async_remove()) @callback def _update_callback(self): diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index 7c48a577850..2e74e079d5f 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -109,7 +109,8 @@ async def async_setup(hass, config): await APPLICATION_CONTROLLER.startup(auto_form=True) for device in APPLICATION_CONTROLLER.devices.values(): - hass.async_add_job(listener.async_device_initialized(device, False)) + hass.async_create_task( + listener.async_device_initialized(device, False)) async def permit(service): """Allow devices to join this network.""" @@ -161,7 +162,8 @@ class ApplicationListener: def device_initialized(self, device): """Handle device joined and basic information discovered.""" - self._hass.async_add_job(self.async_device_initialized(device, True)) + self._hass.async_create_task( + self.async_device_initialized(device, True)) def device_left(self, device): """Handle device leaving the network.""" @@ -170,7 +172,7 @@ class ApplicationListener: def device_removed(self, device): """Handle device being removed from the network.""" for device_entity in self._device_registry[device.ieee]: - self._hass.async_add_job(device_entity.async_remove()) + self._hass.async_create_task(device_entity.async_remove()) async def async_device_initialized(self, device, join): """Handle device joined and basic information discovered (async).""" diff --git a/homeassistant/components/zone/__init__.py b/homeassistant/components/zone/__init__.py index 3754bf5edbc..370b52d1360 100644 --- a/homeassistant/components/zone/__init__.py +++ b/homeassistant/components/zone/__init__.py @@ -55,7 +55,7 @@ async def async_setup(hass, config): entry.get(CONF_ICON), entry.get(CONF_PASSIVE)) zone.entity_id = async_generate_entity_id( ENTITY_ID_FORMAT, entry[CONF_NAME], entities) - hass.async_add_job(zone.async_update_ha_state()) + hass.async_create_task(zone.async_update_ha_state()) entities.add(zone.entity_id) if ENTITY_ID_HOME not in entities and HOME_ZONE not in zone_entries: @@ -63,7 +63,7 @@ async def async_setup(hass, config): hass.config.latitude, hass.config.longitude, DEFAULT_RADIUS, ICON_HOME, False) zone.entity_id = ENTITY_ID_HOME - hass.async_add_job(zone.async_update_ha_state()) + hass.async_create_task(zone.async_update_ha_state()) return True @@ -77,7 +77,7 @@ async def async_setup_entry(hass, config_entry): entry.get(CONF_PASSIVE, DEFAULT_PASSIVE)) zone.entity_id = async_generate_entity_id( ENTITY_ID_FORMAT, name, None, hass) - hass.async_add_job(zone.async_update_ha_state()) + hass.async_create_task(zone.async_update_ha_state()) hass.data[DOMAIN][slugify(name)] = zone return True diff --git a/tests/components/hue/test_bridge.py b/tests/components/hue/test_bridge.py index c20cee0d0e8..ceb30091970 100644 --- a/tests/components/hue/test_bridge.py +++ b/tests/components/hue/test_bridge.py @@ -34,7 +34,7 @@ async def test_bridge_setup_invalid_username(): side_effect=errors.AuthenticationRequired): assert await hue_bridge.async_setup() is False - assert len(hass.async_add_job.mock_calls) == 1 + assert len(hass.async_create_task.mock_calls) == 1 assert len(hass.config_entries.flow.async_init.mock_calls) == 1 assert hass.config_entries.flow.async_init.mock_calls[0][2]['data'] == { 'host': '1.2.3.4' @@ -87,7 +87,7 @@ async def test_reset_if_entry_had_wrong_auth(): side_effect=errors.AuthenticationRequired): assert await hue_bridge.async_setup() is False - assert len(hass.async_add_job.mock_calls) == 1 + assert len(hass.async_create_task.mock_calls) == 1 assert await hue_bridge.async_reset() From 11d5671ee0a4dbf98c5e9f58a2dec85bdb35f4d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Lov=C3=A9n?= Date: Tue, 2 Oct 2018 11:43:34 +0200 Subject: [PATCH 158/247] De-syncing binary_sensor.ping (#17056) * Quick and dirty asyncing of binary_sensor.ping * Minor changes --- .../components/binary_sensor/ping.py | 46 +++++++++++-------- 1 file changed, 26 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/binary_sensor/ping.py b/homeassistant/components/binary_sensor/ping.py index 4c597dd63e1..f12957e6129 100644 --- a/homeassistant/components/binary_sensor/ping.py +++ b/homeassistant/components/binary_sensor/ping.py @@ -4,18 +4,19 @@ Tracks the latency of a host by sending ICMP echo requests (ping). For more details about this platform, please refer to the documentation at https://home-assistant.io/components/binary_sensor.ping/ """ -import logging -import subprocess -import re -import sys +import asyncio from datetime import timedelta +import logging +import re +import subprocess +import sys import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.binary_sensor import ( - BinarySensorDevice, PLATFORM_SCHEMA) -from homeassistant.const import CONF_NAME, CONF_HOST + PLATFORM_SCHEMA, BinarySensorDevice) +from homeassistant.const import CONF_HOST, CONF_NAME +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -48,13 +49,14 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): """Set up the Ping Binary sensor.""" name = config.get(CONF_NAME) host = config.get(CONF_HOST) count = config.get(CONF_PING_COUNT) - add_entities([PingBinarySensor(name, PingData(host, count))], True) + async_add_entities([PingBinarySensor(name, PingData(host, count))], True) class PingBinarySensor(BinarySensorDevice): @@ -91,9 +93,9 @@ class PingBinarySensor(BinarySensorDevice): ATTR_ROUND_TRIP_TIME_MIN: self.ping.data['min'], } - def update(self): + async def async_update(self): """Get the latest data.""" - self.ping.update() + await self.ping.update() class PingData: @@ -114,12 +116,13 @@ class PingData: 'ping', '-n', '-q', '-c', str(self._count), '-W1', self._ip_address] - def ping(self): + async def ping(self): """Send ICMP echo request and return details if success.""" - pinger = subprocess.Popen( - self._ping_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + pinger = await asyncio.create_subprocess_shell( + ' '.join(self._ping_cmd), stdout=subprocess.PIPE, + stderr=subprocess.PIPE) try: - out = pinger.communicate() + out = await pinger.communicate() _LOGGER.debug("Output is %s", str(out)) if sys.platform == 'win32': match = WIN32_PING_MATCHER.search(str(out).split('\n')[-1]) @@ -128,7 +131,8 @@ class PingData: 'min': rtt_min, 'avg': rtt_avg, 'max': rtt_max, - 'mdev': ''} + 'mdev': '', + } if 'max/' not in str(out): match = PING_MATCHER_BUSYBOX.search(str(out).split('\n')[-1]) rtt_min, rtt_avg, rtt_max = match.groups() @@ -136,18 +140,20 @@ class PingData: 'min': rtt_min, 'avg': rtt_avg, 'max': rtt_max, - 'mdev': ''} + 'mdev': '', + } match = PING_MATCHER.search(str(out).split('\n')[-1]) rtt_min, rtt_avg, rtt_max, rtt_mdev = match.groups() return { 'min': rtt_min, 'avg': rtt_avg, 'max': rtt_max, - 'mdev': rtt_mdev} + 'mdev': rtt_mdev, + } except (subprocess.CalledProcessError, AttributeError): return False - def update(self): + async def update(self): """Retrieve the latest details from the host.""" - self.data = self.ping() + self.data = await self.ping() self.available = bool(self.data) From a6f8c3f6620cf9ad0187a09e320feb9d52787577 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 2 Oct 2018 13:33:16 +0200 Subject: [PATCH 159/247] Add logging to light updates (#17069) --- homeassistant/components/light/hue.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/light/hue.py b/homeassistant/components/light/hue.py index 958abaca033..686fc01caf9 100644 --- a/homeassistant/components/light/hue.py +++ b/homeassistant/components/light/hue.py @@ -7,6 +7,7 @@ https://home-assistant.io/components/light.hue/ import asyncio from datetime import timedelta import logging +from time import monotonic import random import async_timeout @@ -159,18 +160,23 @@ async def async_update_items(hass, bridge, async_add_entities, import aiohue if is_group: + api_type = 'group' api = bridge.api.groups else: + api_type = 'light' api = bridge.api.lights try: + start = monotonic() with async_timeout.timeout(4): await api.update() - except (asyncio.TimeoutError, aiohue.AiohueException): + except (asyncio.TimeoutError, aiohue.AiohueException) as err: + _LOGGER.debug('Failed to fetch %s: %s', api_type, err) + if not bridge.available: return - _LOGGER.error('Unable to reach bridge %s', bridge.host) + _LOGGER.error('Unable to reach bridge %s (%s)', bridge.host, err) bridge.available = False for light_id, light in current.items(): @@ -179,6 +185,10 @@ async def async_update_items(hass, bridge, async_add_entities, return + finally: + _LOGGER.debug('Finished %s request in %.3f seconds', + api_type, monotonic() - start) + if not bridge.available: _LOGGER.info('Reconnected to bridge %s', bridge.host) bridge.available = True From 19722a0ef8981ac6496f9df85b0c14fc014e9ab9 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 2 Oct 2018 14:33:30 +0200 Subject: [PATCH 160/247] bump frontend to 20181002.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 083f1a5f0d5..fffb3086d6d 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==20180927.0'] +REQUIREMENTS = ['home-assistant-frontend==20181002.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log', diff --git a/requirements_all.txt b/requirements_all.txt index c93c8a8618c..84d432e582b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -457,7 +457,7 @@ hole==0.3.0 holidays==0.9.7 # homeassistant.components.frontend -home-assistant-frontend==20180927.0 +home-assistant-frontend==20181002.0 # homeassistant.components.homekit_controller # homekit==0.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 80067e607a9..e01cddc7243 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -87,7 +87,7 @@ hdate==0.6.3 holidays==0.9.7 # homeassistant.components.frontend -home-assistant-frontend==20180927.0 +home-assistant-frontend==20181002.0 # homeassistant.components.homematicip_cloud homematicip==0.9.8 From 38e2926a48be3c6a0c3d7fec44bf568b8288f9cc Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 2 Oct 2018 14:33:47 +0200 Subject: [PATCH 161/247] Update translations --- .../components/auth/.translations/de.json | 3 ++- .../components/auth/.translations/hu.json | 16 ++++++++++++++++ .../components/auth/.translations/nl.json | 19 +++++++++++++++++++ .../components/auth/.translations/pl.json | 5 +++-- .../components/auth/.translations/pt.json | 19 +++++++++++++++++++ .../components/cast/.translations/ru.json | 4 ++-- .../components/deconz/.translations/hu.json | 7 ++++++- .../components/deconz/.translations/ru.json | 2 +- .../components/hangouts/.translations/ko.json | 2 +- .../components/hangouts/.translations/nl.json | 2 ++ .../components/hangouts/.translations/ru.json | 2 +- .../homematicip_cloud/.translations/hu.json | 16 ++++++++++++++++ .../homematicip_cloud/.translations/ko.json | 2 +- .../homematicip_cloud/.translations/ru.json | 2 +- .../components/hue/.translations/ko.json | 4 ++-- .../components/hue/.translations/ru.json | 2 +- .../components/ifttt/.translations/ca.json | 18 ++++++++++++++++++ .../components/ifttt/.translations/de.json | 15 +++++++++++++++ .../components/ifttt/.translations/hu.json | 5 +++++ .../components/ifttt/.translations/ko.json | 18 ++++++++++++++++++ .../components/ifttt/.translations/lb.json | 18 ++++++++++++++++++ .../components/ifttt/.translations/nl.json | 18 ++++++++++++++++++ .../components/ifttt/.translations/no.json | 18 ++++++++++++++++++ .../components/ifttt/.translations/pl.json | 18 ++++++++++++++++++ .../components/ifttt/.translations/ru.json | 18 ++++++++++++++++++ .../ifttt/.translations/zh-Hans.json | 18 ++++++++++++++++++ .../ifttt/.translations/zh-Hant.json | 18 ++++++++++++++++++ .../components/ios/.translations/de.json | 6 ++++-- .../components/ios/.translations/ru.json | 4 ++-- .../components/mqtt/.translations/ca.json | 9 ++++++++- .../components/mqtt/.translations/de.json | 2 ++ .../components/mqtt/.translations/en.json | 2 +- .../components/mqtt/.translations/ko.json | 7 +++++++ .../components/mqtt/.translations/lb.json | 7 +++++++ .../components/mqtt/.translations/nl.json | 7 +++++++ .../components/mqtt/.translations/pl.json | 7 +++++++ .../components/mqtt/.translations/pt.json | 1 + .../components/mqtt/.translations/ru.json | 2 +- .../components/nest/.translations/hu.json | 10 ++++++++-- .../components/nest/.translations/ru.json | 2 +- .../components/sonos/.translations/ru.json | 4 ++-- .../components/tradfri/.translations/de.json | 1 + .../components/tradfri/.translations/hu.json | 11 ++++++++++- .../components/tradfri/.translations/pl.json | 3 ++- 44 files changed, 346 insertions(+), 28 deletions(-) create mode 100644 homeassistant/components/ifttt/.translations/ca.json create mode 100644 homeassistant/components/ifttt/.translations/de.json create mode 100644 homeassistant/components/ifttt/.translations/hu.json create mode 100644 homeassistant/components/ifttt/.translations/ko.json create mode 100644 homeassistant/components/ifttt/.translations/lb.json create mode 100644 homeassistant/components/ifttt/.translations/nl.json create mode 100644 homeassistant/components/ifttt/.translations/no.json create mode 100644 homeassistant/components/ifttt/.translations/pl.json create mode 100644 homeassistant/components/ifttt/.translations/ru.json create mode 100644 homeassistant/components/ifttt/.translations/zh-Hans.json create mode 100644 homeassistant/components/ifttt/.translations/zh-Hant.json diff --git a/homeassistant/components/auth/.translations/de.json b/homeassistant/components/auth/.translations/de.json index 21c83290629..4ef4a5bf9e8 100644 --- a/homeassistant/components/auth/.translations/de.json +++ b/homeassistant/components/auth/.translations/de.json @@ -16,7 +16,8 @@ "description": "Ein Einmal-Passwort wurde per ** notify gesendet. {notify_service} **. Bitte gebe es unten ein:", "title": "\u00dcberpr\u00fcfe das Setup" } - } + }, + "title": "Benachrichtig f\u00fcr One-Time Password" }, "totp": { "error": { diff --git a/homeassistant/components/auth/.translations/hu.json b/homeassistant/components/auth/.translations/hu.json index 4500098553e..0a3a3c58820 100644 --- a/homeassistant/components/auth/.translations/hu.json +++ b/homeassistant/components/auth/.translations/hu.json @@ -1,5 +1,21 @@ { "mfa_setup": { + "notify": { + "abort": { + "no_available_service": "Nincs el\u00e9rhet\u0151 \u00e9rtes\u00edt\u00e9si szolg\u00e1ltat\u00e1s." + }, + "error": { + "invalid_code": "\u00c9rv\u00e9nytelen k\u00f3d, pr\u00f3b\u00e1ld \u00fajra." + }, + "step": { + "init": { + "description": "V\u00e1lassz \u00e9rtes\u00edt\u00e9si szolg\u00e1ltat\u00e1st:" + }, + "setup": { + "title": "Be\u00e1ll\u00edt\u00e1s ellen\u0151rz\u00e9se" + } + } + }, "totp": { "error": { "invalid_code": "\u00c9rv\u00e9nytelen k\u00f3d, pr\u00f3b\u00e1ld \u00fajra. Ha ez a hiba folyamatosan el\u0151fordul, akkor gy\u0151z\u0151dj meg r\u00f3la, hogy a Home Assistant rendszered \u00f3r\u00e1ja pontosan j\u00e1r." diff --git a/homeassistant/components/auth/.translations/nl.json b/homeassistant/components/auth/.translations/nl.json index 40a873023dd..9ec8006507b 100644 --- a/homeassistant/components/auth/.translations/nl.json +++ b/homeassistant/components/auth/.translations/nl.json @@ -1,5 +1,24 @@ { "mfa_setup": { + "notify": { + "abort": { + "no_available_service": "Geen meldingsservices beschikbaar." + }, + "error": { + "invalid_code": "Ongeldige code, probeer opnieuw." + }, + "step": { + "init": { + "description": "Selecteer een van de meldingsdiensten:", + "title": "Stel een \u00e9\u00e9nmalig wachtwoord in dat wordt afgegeven door een meldingscomponent" + }, + "setup": { + "description": "Een \u00e9\u00e9nmalig wachtwoord is verzonden via **notify. {notify_service}**. Voer het hieronder in:", + "title": "Controleer de instellingen" + } + }, + "title": "Eenmalig wachtwoord melden" + }, "totp": { "error": { "invalid_code": "Ongeldige code, probeer het opnieuw. Als u deze fout blijft krijgen, controleer dan of de klok van uw Home Assistant systeem correct is ingesteld." diff --git a/homeassistant/components/auth/.translations/pl.json b/homeassistant/components/auth/.translations/pl.json index 3e320ba8d62..6adaaa019c5 100644 --- a/homeassistant/components/auth/.translations/pl.json +++ b/homeassistant/components/auth/.translations/pl.json @@ -13,10 +13,11 @@ "title": "Skonfiguruj has\u0142o jednorazowe dostarczone przez komponent powiadomie\u0144" }, "setup": { - "description": "Has\u0142o jednorazowe zosta\u0142o wys\u0142ane przez ** powiadom. {notify_service} **. Wpisz je poni\u017cej:", + "description": "Has\u0142o jednorazowe zosta\u0142o wys\u0142ane przez **notify.{notify_service}**. Wpisz je poni\u017cej:", "title": "Sprawd\u017a konfiguracj\u0119" } - } + }, + "title": "Powiadomienie z has\u0142em jednorazowym" }, "totp": { "error": { diff --git a/homeassistant/components/auth/.translations/pt.json b/homeassistant/components/auth/.translations/pt.json index 474dbe488be..5401c0117e6 100644 --- a/homeassistant/components/auth/.translations/pt.json +++ b/homeassistant/components/auth/.translations/pt.json @@ -1,5 +1,24 @@ { "mfa_setup": { + "notify": { + "abort": { + "no_available_service": "Nenhum servi\u00e7o de notifica\u00e7\u00e3o dispon\u00edvel." + }, + "error": { + "invalid_code": "C\u00f3digo inv\u00e1lido, por favor tente novamente." + }, + "step": { + "init": { + "description": "Por favor, selecione um dos servi\u00e7os de notifica\u00e7\u00e3o:", + "title": "Configurar uma palavra passe entregue pela componente de notifica\u00e7\u00e3o" + }, + "setup": { + "description": "Foi enviada uma palavra passe atrav\u00e9s de **notify.{notify_service}**. Por favor, insira-a:", + "title": "Verificar a configura\u00e7\u00e3o" + } + }, + "title": "Notificar palavra passe de uso \u00fanico" + }, "totp": { "error": { "invalid_code": "C\u00f3digo inv\u00e1lido, por favor, tente novamente. Se receber este erro constantemente, por favor, certifique-se de que o rel\u00f3gio do sistema que hospeda o Home Assistent \u00e9 preciso." diff --git a/homeassistant/components/cast/.translations/ru.json b/homeassistant/components/cast/.translations/ru.json index 9c9353da37e..da03eae701d 100644 --- a/homeassistant/components/cast/.translations/ru.json +++ b/homeassistant/components/cast/.translations/ru.json @@ -2,11 +2,11 @@ "config": { "abort": { "no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 Google Cast \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b.", - "single_instance_allowed": "\u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u0430 \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f Google Cast." + "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 \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Google Cast?", + "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 Google Cast?", "title": "Google Cast" } }, diff --git a/homeassistant/components/deconz/.translations/hu.json b/homeassistant/components/deconz/.translations/hu.json index c1fd76c5035..ca2466e9921 100644 --- a/homeassistant/components/deconz/.translations/hu.json +++ b/homeassistant/components/deconz/.translations/hu.json @@ -19,8 +19,13 @@ "link": { "description": "Oldja fel a deCONZ \u00e1tj\u00e1r\u00f3t a Home Assistant-ban val\u00f3 regisztr\u00e1l\u00e1shoz.\n\n1. Menjen a deCONZ rendszer be\u00e1ll\u00edt\u00e1sokhoz\n2. Nyomja meg az \"\u00c1tj\u00e1r\u00f3 felold\u00e1sa\" gombot", "title": "Kapcsol\u00f3d\u00e1s a deCONZ-hoz" + }, + "options": { + "data": { + "allow_clip_sensor": "Virtu\u00e1lis szenzorok import\u00e1l\u00e1s\u00e1nak enged\u00e9lyez\u00e9se" + } } }, - "title": "deCONZ" + "title": "deCONZ Zigbee gateway" } } \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/ru.json b/homeassistant/components/deconz/.translations/ru.json index 56490f67cb3..a9b66314f31 100644 --- a/homeassistant/components/deconz/.translations/ru.json +++ b/homeassistant/components/deconz/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u0428\u043b\u044e\u0437 \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d", + "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.", "no_bridges": "\u0428\u043b\u044e\u0437\u044b deCONZ \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b", "one_instance_only": "\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u0438\u043d \u044d\u043a\u0437\u0435\u043c\u043f\u043b\u044f\u0440 deCONZ" }, diff --git a/homeassistant/components/hangouts/.translations/ko.json b/homeassistant/components/hangouts/.translations/ko.json index aabf977a8cc..af0e76829e5 100644 --- a/homeassistant/components/hangouts/.translations/ko.json +++ b/homeassistant/components/hangouts/.translations/ko.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Google Hangouts \uc740 \uc774\ubbf8 \uc124\uc815\ub41c \uc0c1\ud0dc\uc785\ub2c8\ub2e4", - "unknown": "\uc54c \uc218\uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + "unknown": "\uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, "error": { "invalid_2fa": "2\ub2e8\uacc4 \uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574 \uc8fc\uc138\uc694.", diff --git a/homeassistant/components/hangouts/.translations/nl.json b/homeassistant/components/hangouts/.translations/nl.json index cf73210aa3b..da9bc9edd7b 100644 --- a/homeassistant/components/hangouts/.translations/nl.json +++ b/homeassistant/components/hangouts/.translations/nl.json @@ -14,6 +14,7 @@ "data": { "2fa": "2FA pin" }, + "description": "Leeg", "title": "Twee-factor-authenticatie" }, "user": { @@ -21,6 +22,7 @@ "email": "E-mailadres", "password": "Wachtwoord" }, + "description": "Leeg", "title": "Google Hangouts inlog" } }, diff --git a/homeassistant/components/hangouts/.translations/ru.json b/homeassistant/components/hangouts/.translations/ru.json index c3363215201..6d93ec0d18f 100644 --- a/homeassistant/components/hangouts/.translations/ru.json +++ b/homeassistant/components/hangouts/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Google Hangouts \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d", + "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.", "unknown": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430" }, "error": { diff --git a/homeassistant/components/homematicip_cloud/.translations/hu.json b/homeassistant/components/homematicip_cloud/.translations/hu.json index f2f22e6a49d..cfb4f5e87fd 100644 --- a/homeassistant/components/homematicip_cloud/.translations/hu.json +++ b/homeassistant/components/homematicip_cloud/.translations/hu.json @@ -1,5 +1,21 @@ { "config": { + "abort": { + "connection_aborted": "Nem siker\u00fclt csatlakozni a HMIP szerverhez", + "unknown": "Unknown error occurred." + }, + "error": { + "invalid_pin": "\u00c9rv\u00e9nytelen PIN, pr\u00f3b\u00e1lkozz \u00fajra.", + "press_the_button": "Nyomd meg a k\u00e9k gombot.", + "register_failed": "Regisztr\u00e1ci\u00f3 nem siker\u00fclt, pr\u00f3b\u00e1ld \u00fajra." + }, + "step": { + "init": { + "data": { + "pin": "Pin k\u00f3d (opcion\u00e1lis)" + } + } + }, "title": "HomematicIP Felh\u0151" } } \ No newline at end of file diff --git a/homeassistant/components/homematicip_cloud/.translations/ko.json b/homeassistant/components/homematicip_cloud/.translations/ko.json index 7b8dc8b5087..46ef55c9eca 100644 --- a/homeassistant/components/homematicip_cloud/.translations/ko.json +++ b/homeassistant/components/homematicip_cloud/.translations/ko.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "\uc561\uc138\uc2a4 \ud3ec\uc778\ud2b8\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "connection_aborted": "HMIP \uc11c\ubc84\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", - "unknown": "\uc54c \uc218\uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + "unknown": "\uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, "error": { "invalid_pin": "PIN\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", diff --git a/homeassistant/components/homematicip_cloud/.translations/ru.json b/homeassistant/components/homematicip_cloud/.translations/ru.json index ef2b3be4a64..6a0ea612fe8 100644 --- a/homeassistant/components/homematicip_cloud/.translations/ru.json +++ b/homeassistant/components/homematicip_cloud/.translations/ru.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "\u0422\u043e\u0447\u043a\u0430 \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430", "connection_aborted": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0441\u0435\u0440\u0432\u0435\u0440\u0443 HMIP", - "unknown": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430" + "unknown": "\u0412\u043e\u0437\u043d\u0438\u043a\u043b\u0430 \u043d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "error": { "invalid_pin": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 PIN-\u043a\u043e\u0434, \u043f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0441\u043d\u043e\u0432\u0430.", diff --git a/homeassistant/components/hue/.translations/ko.json b/homeassistant/components/hue/.translations/ko.json index 47306a35414..48fe4e8af28 100644 --- a/homeassistant/components/hue/.translations/ko.json +++ b/homeassistant/components/hue/.translations/ko.json @@ -6,10 +6,10 @@ "cannot_connect": "\ube0c\ub9ac\uc9c0\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", "discover_timeout": "Hue \ube0c\ub9bf\uc9c0\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", "no_bridges": "\ubc1c\uacac\ub41c \ud544\ub9bd\uc2a4 Hue \ube0c\ub9bf\uc9c0\uac00 \uc5c6\uc2b5\ub2c8\ub2e4", - "unknown": "\uc54c \uc218\uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + "unknown": "\uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, "error": { - "linking": "\uc54c \uc218\uc5c6\ub294 \uc5f0\uacb0 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.", + "linking": "\uc54c \uc218 \uc5c6\ub294 \uc5f0\uacb0 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.", "register_failed": "\ub4f1\ub85d\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\ubcf4\uc138\uc694" }, "step": { diff --git a/homeassistant/components/hue/.translations/ru.json b/homeassistant/components/hue/.translations/ru.json index b471dd1a0cd..4b2581dde65 100644 --- a/homeassistant/components/hue/.translations/ru.json +++ b/homeassistant/components/hue/.translations/ru.json @@ -2,7 +2,7 @@ "config": { "abort": { "all_configured": "\u0412\u0441\u0435 Philips Hue \u0448\u043b\u044e\u0437\u044b \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u044b", - "already_configured": "\u0428\u043b\u044e\u0437 \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d", + "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.", "cannot_connect": "\u041d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0448\u043b\u044e\u0437\u0443", "discover_timeout": "\u041d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0442\u044c \u0448\u043b\u044e\u0437\u044b Philips Hue", "no_bridges": "\u0428\u043b\u044e\u0437\u044b Philips Hue \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b", diff --git a/homeassistant/components/ifttt/.translations/ca.json b/homeassistant/components/ifttt/.translations/ca.json new file mode 100644 index 00000000000..f93fbe19078 --- /dev/null +++ b/homeassistant/components/ifttt/.translations/ca.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "La vostra inst\u00e0ncia de Home Assistant ha de ser accessible des d'Internet per rebre missatges de IFTTT.", + "one_instance_allowed": "Nom\u00e9s cal una sola inst\u00e0ncia." + }, + "create_entry": { + "default": "Per enviar esdeveniments a Home Assistant, necessitareu utilitzar l'acci\u00f3 \"Make a web resquest\" de [IFTTT Webhook applet]({applet_url}). \n\n Ompliu la informaci\u00f3 seg\u00fcent: \n\n - URL: `{webhook_url}` \n - Method: POST \n - Content Type: application/json \n\nConsulteu [la documentaci\u00f3]({docs_url}) sobre com configurar els automatismes per gestionar les dades entrants." + }, + "step": { + "user": { + "description": "Esteu segur que voleu configurar IFTTT?", + "title": "Configureu la miniaplicaci\u00f3 Webhook de IFTTT" + } + }, + "title": "IFTTT" + } +} \ No newline at end of file diff --git a/homeassistant/components/ifttt/.translations/de.json b/homeassistant/components/ifttt/.translations/de.json new file mode 100644 index 00000000000..da2cec98c72 --- /dev/null +++ b/homeassistant/components/ifttt/.translations/de.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Auf Ihre Home Assistant-Instanz muss vom Internet aus zugegriffen werden k\u00f6nnen, um IFTTT-Nachrichten zu empfangen.", + "one_instance_allowed": "Nur eine einzige Instanz ist notwendig." + }, + "step": { + "user": { + "description": "Bist du sicher, dass du IFTTT einrichten m\u00f6chtest?", + "title": "Einrichten des IFTTT Webhook Applets" + } + }, + "title": "IFTTT" + } +} \ No newline at end of file diff --git a/homeassistant/components/ifttt/.translations/hu.json b/homeassistant/components/ifttt/.translations/hu.json new file mode 100644 index 00000000000..077956287b3 --- /dev/null +++ b/homeassistant/components/ifttt/.translations/hu.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "IFTTT" + } +} \ No newline at end of file diff --git a/homeassistant/components/ifttt/.translations/ko.json b/homeassistant/components/ifttt/.translations/ko.json new file mode 100644 index 00000000000..1a53480e1b1 --- /dev/null +++ b/homeassistant/components/ifttt/.translations/ko.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "IFTTT \uba54\uc2dc\uc9c0\ub97c \ubc1b\uc73c\ub824\uba74 \uc778\ud130\ub137\uc5d0\uc11c \ud648 \uc5b4\uc2dc\uc2a4\ud134\ud2b8 \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\uae30 \uc704\ud574\uc11c\ub294 [IFTTT Webhook \uc560\ud50c\ub9bf]({applet_url}) \uc5d0\uc11c \"Make a web request\" \uc791\uc5c5\uc744 \uc0ac\uc6a9\ud574\uc57c \ud569\ub2c8\ub2e4. \n\n \ub2e4\uc74c\uc758 \uc815\ubcf4\ub97c \uc785\ub825\ud574 \uc8fc\uc138\uc694.\n\n - URL: `{webhook_url}` \n - Method: POST \n - Content Type: application/json \n\n Home Assistant \ub85c \ub4e4\uc5b4\uc624\ub294 \ub370\uc774\ud130\ub97c \ucc98\ub9ac\ud558\uae30 \uc704\ud55c \uc790\ub3d9\ud654\ub97c \uad6c\uc131\ud558\ub294 \ubc29\ubc95\uc740 [\ubcf8 \ubb38\uc11c]({docs_url}) \ub97c \ucc38\uc870\ud574 \uc8fc\uc138\uc694." + }, + "step": { + "user": { + "description": "IFTTT \ub97c \uc124\uc815 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "title": "IFTTT Webhook Applet \uc124\uc815" + } + }, + "title": "IFTTT" + } +} \ No newline at end of file diff --git a/homeassistant/components/ifttt/.translations/lb.json b/homeassistant/components/ifttt/.translations/lb.json new file mode 100644 index 00000000000..74e6b4926ef --- /dev/null +++ b/homeassistant/components/ifttt/.translations/lb.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "\u00c4r Home Assistant Instanz muss iwwert Internet accessibel si fir IFTTT 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\u00e9ckemusst dir d'Aktioun \"Make a web request\" vum [IFTTT Webhook applet] ({applet_url}) benotzen.\n\nGitt folgend Informatiounen un:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\nKuckt iech [Dokumentatioun]({docs_url}) w\u00e9i een Automatisatioune mat empfaangene Donn\u00e9e konfigur\u00e9iert." + }, + "step": { + "user": { + "description": "S\u00e9cher fir IFTTT anzeriichten?", + "title": "IFTTT Webhook Applet ariichten" + } + }, + "title": "IFTTT" + } +} \ No newline at end of file diff --git a/homeassistant/components/ifttt/.translations/nl.json b/homeassistant/components/ifttt/.translations/nl.json new file mode 100644 index 00000000000..9188b1f6b08 --- /dev/null +++ b/homeassistant/components/ifttt/.translations/nl.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Uw Home Assistant-instantie moet via internet toegankelijk zijn om IFTTT-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 de actie \"Een webverzoek doen\" gebruiken vanuit de [IFTTT Webhook-applet]({applet_url}). \n\n Vul de volgende info in: \n\n - URL: `{webhook_url}` \n - Method: POST \n - Content Type: application/json \n\nZie [the documentation]({docs_url}) voor informatie over het configureren van automatiseringen om inkomende gegevens te verwerken." + }, + "step": { + "user": { + "description": "Weet je zeker dat u IFTTT wilt instellen?", + "title": "Stel de IFTTT Webhook-applet in" + } + }, + "title": "IFTTT" + } +} \ No newline at end of file diff --git a/homeassistant/components/ifttt/.translations/no.json b/homeassistant/components/ifttt/.translations/no.json new file mode 100644 index 00000000000..481ab372e91 --- /dev/null +++ b/homeassistant/components/ifttt/.translations/no.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Din Home Assistant enhet m\u00e5 v\u00e6re tilgjengelig fra internett for \u00e5 kunne motta IFTTT-meldinger.", + "one_instance_allowed": "Kun en enkelt forekomst er n\u00f8dvendig." + }, + "create_entry": { + "default": "For \u00e5 sende hendelser til Home Assistant, m\u00e5 du bruke \"Make a web request\" handlingen fra [IFTTT Webhook applet]({applet_url}).\n\nFyll ut f\u00f8lgende informasjon:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\nSe [dokumentasjonen]({docs_url}) om hvordan du konfigurerer automatiseringer for \u00e5 h\u00e5ndtere innkommende data." + }, + "step": { + "user": { + "description": "Er du sikker p\u00e5 at du vil sette opp IFTTT?", + "title": "Sett opp IFTTT Webhook Applet" + } + }, + "title": "IFTTT" + } +} \ No newline at end of file diff --git a/homeassistant/components/ifttt/.translations/pl.json b/homeassistant/components/ifttt/.translations/pl.json new file mode 100644 index 00000000000..3c3c2182503 --- /dev/null +++ b/homeassistant/components/ifttt/.translations/pl.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Tw\u00f3j Home Assistant musi by\u0107 dost\u0119pny z Internetu, aby odbiera\u0107 komunikaty IFTTT.", + "one_instance_allowed": "Wymagana jest tylko jedna instancja." + }, + "create_entry": { + "default": "Aby wys\u0142a\u0107 zdarzenia do Home Assistant'a, b\u0119dziesz musia\u0142 u\u017cy\u0107 akcji \"Make a web request\" z [IFTTT Webhook apletu]({applet_url}). \n\n Podaj nast\u0119puj\u0105ce informacje:\n\n - URL: `{webhook_url}`\n - Metoda: POST\n - Typ zawarto\u015bci: application/json\n\nZapoznaj si\u0119 z [dokumentacj\u0105]({docs_url}) na temat konfiguracji automatyzacji by obs\u0142u\u017cy\u0107 przychodz\u0105ce dane." + }, + "step": { + "user": { + "description": "Jeste\u015b pewny, \u017ce chcesz skonfigurowa\u0107 IFTTT?", + "title": "Konfigurowanie apletu Webhook IFTTT" + } + }, + "title": "IFTTT" + } +} \ No newline at end of file diff --git a/homeassistant/components/ifttt/.translations/ru.json b/homeassistant/components/ifttt/.translations/ru.json new file mode 100644 index 00000000000..3c1d7b580e4 --- /dev/null +++ b/homeassistant/components/ifttt/.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 IFTTT.", + "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 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0435 \"Make a web request\" \u0438\u0437 [IFTTT Webhook applet]({applet_url}).\n\n\u0417\u0430\u043f\u043e\u043b\u043d\u0438\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- Content Type: application/json\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 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0437\u0430\u0446\u0438\u0439 \u043f\u043e \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0435 \u043f\u043e\u0441\u0442\u0443\u043f\u0430\u044e\u0449\u0438\u0445 \u0434\u0430\u043d\u043d\u044b\u0445." + }, + "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 IFTTT?", + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 IFTTT Webhook Applet" + } + }, + "title": "IFTTT" + } +} \ No newline at end of file diff --git a/homeassistant/components/ifttt/.translations/zh-Hans.json b/homeassistant/components/ifttt/.translations/zh-Hans.json new file mode 100644 index 00000000000..66e667d0e47 --- /dev/null +++ b/homeassistant/components/ifttt/.translations/zh-Hans.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "\u60a8\u7684 Home Assistant \u5b9e\u4f8b\u9700\u8981\u53ef\u4ece\u4e92\u8054\u7f51\u8bbf\u95ee\u4ee5\u63a5\u6536 IFTTT \u6d88\u606f\u3002", + "one_instance_allowed": "\u53ea\u6709\u4e00\u4e2a\u5b9e\u4f8b\u662f\u5fc5\u9700\u7684\u3002" + }, + "create_entry": { + "default": "\u8981\u5411 Home Assistant \u53d1\u9001\u4e8b\u4ef6\uff0c\u60a8\u9700\u8981\u4f7f\u7528 [IFTTT Webhook \u5c0f\u7a0b\u5e8f]({applet_url}) \u4e2d\u7684 \"Make a web request\" \u52a8\u4f5c\u3002\n\n\u586b\u5199\u4ee5\u4e0b\u4fe1\u606f\uff1a\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\n\u6709\u5173\u5982\u4f55\u914d\u7f6e\u81ea\u52a8\u5316\u4ee5\u5904\u7406\u4f20\u5165\u7684\u6570\u636e\uff0c\u8bf7\u53c2\u9605[\u6587\u6863]({docs_url})\u3002" + }, + "step": { + "user": { + "description": "\u60a8\u786e\u5b9a\u8981\u8bbe\u7f6e IFTTT \u5417\uff1f", + "title": "\u8bbe\u7f6e IFTTT Webhook \u5c0f\u7a0b\u5e8f" + } + }, + "title": "IFTTT" + } +} \ No newline at end of file diff --git a/homeassistant/components/ifttt/.translations/zh-Hant.json b/homeassistant/components/ifttt/.translations/zh-Hant.json new file mode 100644 index 00000000000..5c75beddbe1 --- /dev/null +++ b/homeassistant/components/ifttt/.translations/zh-Hant.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Home Assistant \u5be6\u4f8b\u5fc5\u9808\u80fd\u5920\u7531\u7db2\u969b\u7db2\u8def\u5b58\u53d6\uff0c\u65b9\u80fd\u63a5\u53d7 IFTTT \u8a0a\u606f\u3002", + "one_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u7d44\u5be6\u4f8b\u5373\u53ef\u3002" + }, + "create_entry": { + "default": "\u6b32\u50b3\u9001\u4e8b\u4ef6\u81f3 Home Assistant\uff0c\u5c07\u9700\u8981\u7531 [IFTTT Webhook applet]({applet_url}) \u547c\u53eb\u300c\u9032\u884c Web \u8acb\u6c42\u300d\u52d5\u4f5c\u3002\n\n\u8acb\u586b\u5beb\u4e0b\u5217\u8cc7\u8a0a\uff1a\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\n\u95dc\u65bc\u5982\u4f55\u50b3\u5165\u8cc7\u6599\u81ea\u52d5\u5316\u8a2d\u5b9a\uff0c\u8acb\u53c3\u95b1[\u6587\u4ef6]({docs_url})\u4ee5\u9032\u884c\u4e86\u89e3\u3002" + }, + "step": { + "user": { + "description": "\u662f\u5426\u8981\u8a2d\u5b9a IFTTT\uff1f", + "title": "\u8a2d\u5b9a IFTTT Webhook Applet" + } + }, + "title": "IFTTT" + } +} \ No newline at end of file diff --git a/homeassistant/components/ios/.translations/de.json b/homeassistant/components/ios/.translations/de.json index e9e592d18c2..18ffda135ee 100644 --- a/homeassistant/components/ios/.translations/de.json +++ b/homeassistant/components/ios/.translations/de.json @@ -5,8 +5,10 @@ }, "step": { "confirm": { - "description": "M\u00f6chtest du die Home Assistant iOS-Komponente einrichten?" + "description": "M\u00f6chtest du die Home Assistant iOS-Komponente einrichten?", + "title": "Home Assistant iOS" } - } + }, + "title": "Home Assistant iOS" } } \ No newline at end of file diff --git a/homeassistant/components/ios/.translations/ru.json b/homeassistant/components/ios/.translations/ru.json index 7030f18b729..fdcc964a0e6 100644 --- a/homeassistant/components/ios/.translations/ru.json +++ b/homeassistant/components/ios/.translations/ru.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "single_instance_allowed": "\u0414\u043e\u043f\u0443\u0441\u043a\u0430\u0435\u0442\u0441\u044f \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f Home Assistant iOS." + "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 \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 Home Assistant iOS?", + "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 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 Home Assistant iOS?", "title": "Home Assistant iOS" } }, diff --git a/homeassistant/components/mqtt/.translations/ca.json b/homeassistant/components/mqtt/.translations/ca.json index b6c73f35f26..72a2636fb60 100644 --- a/homeassistant/components/mqtt/.translations/ca.json +++ b/homeassistant/components/mqtt/.translations/ca.json @@ -10,13 +10,20 @@ "broker": { "data": { "broker": "Broker", - "discovery": "Activar descobreix automaticament", + "discovery": "Habilita descobriment autom\u00e0tic", "password": "Contrasenya", "port": "Port", "username": "Nom d'usuari" }, "description": "Introdu\u00efu la informaci\u00f3 de connexi\u00f3 del vostre broker MQTT.", "title": "MQTT" + }, + "hassio_confirm": { + "data": { + "discovery": "Habilita descobriment autom\u00e0tic" + }, + "description": "Voleu configurar Home Assistant perqu\u00e8 es connecti amb el broker MQTT proporcionat pel complement de hass.io {addon}?", + "title": "Broker MQTT a trav\u00e9s del complement de Hass.io" } }, "title": "MQTT" diff --git a/homeassistant/components/mqtt/.translations/de.json b/homeassistant/components/mqtt/.translations/de.json index 2a35e95f559..f007e23bb35 100644 --- a/homeassistant/components/mqtt/.translations/de.json +++ b/homeassistant/components/mqtt/.translations/de.json @@ -9,8 +9,10 @@ "step": { "broker": { "data": { + "broker": "Server", "discovery": "Suche aktivieren", "password": "Passwort", + "port": "Port", "username": "Benutzername" }, "description": "Bitte gib die Verbindungsinformationen deines MQTT-Brokers ein.", diff --git a/homeassistant/components/mqtt/.translations/en.json b/homeassistant/components/mqtt/.translations/en.json index 97c0c1c7091..b0de6dcd782 100644 --- a/homeassistant/components/mqtt/.translations/en.json +++ b/homeassistant/components/mqtt/.translations/en.json @@ -28,4 +28,4 @@ }, "title": "MQTT" } -} +} \ No newline at end of file diff --git a/homeassistant/components/mqtt/.translations/ko.json b/homeassistant/components/mqtt/.translations/ko.json index f20658d252c..571d01b9884 100644 --- a/homeassistant/components/mqtt/.translations/ko.json +++ b/homeassistant/components/mqtt/.translations/ko.json @@ -17,6 +17,13 @@ }, "description": "MQTT \ube0c\ub85c\ucee4\uc640\uc758 \uc5f0\uacb0 \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694.", "title": "MQTT" + }, + "hassio_confirm": { + "data": { + "discovery": "\uae30\uae30 \uac80\uc0c9 \ud65c\uc131\ud654" + }, + "description": "Hass.io \uc560\ub4dc\uc628 {addon} \uc5d0\uc11c \uc81c\uacf5\ud558\ub294 MQTT \ube0c\ub85c\ucee4\uc5d0 \uc5f0\uacb0\ud558\ub3c4\ub85d Home Assistant \ub97c \uad6c\uc131 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "title": "Hass.io \uc560\ub4dc\uc628\uc744 \ud1b5\ud55c MQTT \ube0c\ub85c\ucee4" } }, "title": "MQTT" diff --git a/homeassistant/components/mqtt/.translations/lb.json b/homeassistant/components/mqtt/.translations/lb.json index 166fce9fbfb..9dcd9c58a3a 100644 --- a/homeassistant/components/mqtt/.translations/lb.json +++ b/homeassistant/components/mqtt/.translations/lb.json @@ -17,6 +17,13 @@ }, "description": "Gitt Verbindungs Informatioune vun \u00e4rem MQTT Broker an.", "title": "MQTT" + }, + "hassio_confirm": { + "data": { + "discovery": "Entdeckung aktiv\u00e9ieren" + }, + "description": "W\u00ebllt dir Home Assistant konfigur\u00e9iere fir sech mam MQTT broker ze verbannen dee vum hass.io add-on {addon} bereet gestallt g\u00ebtt?", + "title": "MQTT Broker via Hass.io add-on" } }, "title": "MQTT" diff --git a/homeassistant/components/mqtt/.translations/nl.json b/homeassistant/components/mqtt/.translations/nl.json index b375f353810..e66a4944fcc 100644 --- a/homeassistant/components/mqtt/.translations/nl.json +++ b/homeassistant/components/mqtt/.translations/nl.json @@ -1,9 +1,16 @@ { "config": { + "abort": { + "single_instance_allowed": "Slechts \u00e9\u00e9n configuratie van MQTT is toegestaan." + }, + "error": { + "cannot_connect": "Kan geen verbinding maken met de broker." + }, "step": { "broker": { "data": { "broker": "Broker", + "discovery": "Detectie inschakelen", "password": "Wachtwoord", "port": "Poort", "username": "Gebruikersnaam" diff --git a/homeassistant/components/mqtt/.translations/pl.json b/homeassistant/components/mqtt/.translations/pl.json index e87e550b98d..33c33c5c095 100644 --- a/homeassistant/components/mqtt/.translations/pl.json +++ b/homeassistant/components/mqtt/.translations/pl.json @@ -17,6 +17,13 @@ }, "description": "Wprowad\u017a informacje o po\u0142\u0105czeniu po\u015brednika MQTT.", "title": "MQTT" + }, + "hassio_confirm": { + "data": { + "discovery": "W\u0142\u0105cz wykrywanie" + }, + "description": "Czy chcesz skonfigurowa\u0107 Home Assistant'a, aby po\u0142\u0105czy\u0142 si\u0119 z po\u015brednikiem MQTT dostarczonym przez dodatek Hass.io {addon}?", + "title": "Po\u015brednik MQTT za po\u015brednictwem dodatku Hass.io" } }, "title": "MQTT" diff --git a/homeassistant/components/mqtt/.translations/pt.json b/homeassistant/components/mqtt/.translations/pt.json index 42f7c7f5ad2..1b8c3946b7c 100644 --- a/homeassistant/components/mqtt/.translations/pt.json +++ b/homeassistant/components/mqtt/.translations/pt.json @@ -10,6 +10,7 @@ "broker": { "data": { "broker": "", + "discovery": "Ativar descoberta", "password": "Palavra-passe", "port": "Porto", "username": "Utilizador" diff --git a/homeassistant/components/mqtt/.translations/ru.json b/homeassistant/components/mqtt/.translations/ru.json index f1ff498dd72..745fad31043 100644 --- a/homeassistant/components/mqtt/.translations/ru.json +++ b/homeassistant/components/mqtt/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u0414\u043e\u043f\u0443\u0441\u043a\u0430\u0435\u0442\u0441\u044f \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f MQTT." + "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." }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u0435\u0442\u0441\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0431\u0440\u043e\u043a\u0435\u0440\u0443." diff --git a/homeassistant/components/nest/.translations/hu.json b/homeassistant/components/nest/.translations/hu.json index abf8f79599f..142747a016f 100644 --- a/homeassistant/components/nest/.translations/hu.json +++ b/homeassistant/components/nest/.translations/hu.json @@ -1,13 +1,19 @@ { "config": { + "abort": { + "authorize_url_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s az \u00e9rv\u00e9nyes\u00edt\u00e9si url gener\u00e1l\u00e1sa sor\u00e1n." + }, "error": { - "invalid_code": "\u00c9rv\u00e9nytelen k\u00f3d" + "invalid_code": "\u00c9rv\u00e9nytelen k\u00f3d", + "timeout": "Id\u0151t\u00fall\u00e9p\u00e9s a k\u00f3d \u00e9rv\u00e9nyes\u00edt\u00e9se sor\u00e1n.", + "unknown": "Ismeretlen hiba t\u00f6rt\u00e9nt a k\u00f3d \u00e9rv\u00e9nyes\u00edt\u00e9se sor\u00e1n" }, "step": { "init": { "data": { "flow_impl": "Szolg\u00e1ltat\u00f3" - } + }, + "title": "Hiteles\u00edt\u00e9si Szolg\u00e1ltat\u00f3" }, "link": { "data": { diff --git a/homeassistant/components/nest/.translations/ru.json b/homeassistant/components/nest/.translations/ru.json index 0f7b9b8dd71..6a73bd47203 100644 --- a/homeassistant/components/nest/.translations/ru.json +++ b/homeassistant/components/nest/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_setup": "\u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0443 \u0443\u0447\u0435\u0442\u043d\u0443\u044e \u0437\u0430\u043f\u0438\u0441\u044c Nest.", + "already_setup": "\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.", "authorize_url_fail": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u0438 \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0438 \u0441\u0441\u044b\u043b\u043a\u0438 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438.", "authorize_url_timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0438 \u0441\u0441\u044b\u043b\u043a\u0438 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438.", "no_flows": "\u0412\u0430\u043c \u043d\u0443\u0436\u043d\u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Nest \u043f\u0435\u0440\u0435\u0434 \u0442\u0435\u043c, \u043a\u0430\u043a \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044e. [\u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438](https://www.home-assistant.io/components/nest/)." diff --git a/homeassistant/components/sonos/.translations/ru.json b/homeassistant/components/sonos/.translations/ru.json index 63b6bd87c20..1bff827d273 100644 --- a/homeassistant/components/sonos/.translations/ru.json +++ b/homeassistant/components/sonos/.translations/ru.json @@ -2,11 +2,11 @@ "config": { "abort": { "no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 Sonos \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b.", - "single_instance_allowed": "\u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u0430 \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f Sonos." + "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 \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Sonos?", + "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 Sonos?", "title": "Sonos" } }, diff --git a/homeassistant/components/tradfri/.translations/de.json b/homeassistant/components/tradfri/.translations/de.json index 5284ae18b6d..4a19972774d 100644 --- a/homeassistant/components/tradfri/.translations/de.json +++ b/homeassistant/components/tradfri/.translations/de.json @@ -11,6 +11,7 @@ "step": { "auth": { "data": { + "host": "Host", "security_code": "Sicherheitscode" }, "description": "Du findest den Sicherheitscode auf der R\u00fcckseite deines Gateways.", diff --git a/homeassistant/components/tradfri/.translations/hu.json b/homeassistant/components/tradfri/.translations/hu.json index 0844e6d7095..dc7c033d41d 100644 --- a/homeassistant/components/tradfri/.translations/hu.json +++ b/homeassistant/components/tradfri/.translations/hu.json @@ -1,11 +1,20 @@ { "config": { + "abort": { + "already_configured": "A bridge m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "cannot_connect": "Nem siker\u00fclt csatlakozni a gatewayhez.", + "timeout": "Id\u0151t\u00fall\u00e9p\u00e9s a k\u00f3d \u00e9rv\u00e9nyes\u00edt\u00e9se sor\u00e1n." + }, "step": { "auth": { "data": { "host": "Hoszt", "security_code": "Biztons\u00e1gi K\u00f3d" - } + }, + "description": "A biztons\u00e1gi k\u00f3dot a Gatewayed h\u00e1toldal\u00e1n tal\u00e1lod.", + "title": "Add meg a biztons\u00e1gi k\u00f3dot" } }, "title": "IKEA TR\u00c5DFRI" diff --git a/homeassistant/components/tradfri/.translations/pl.json b/homeassistant/components/tradfri/.translations/pl.json index ec253447ef4..4fd71567afe 100644 --- a/homeassistant/components/tradfri/.translations/pl.json +++ b/homeassistant/components/tradfri/.translations/pl.json @@ -17,6 +17,7 @@ "description": "Mo\u017cesz znale\u017a\u0107 kod bezpiecze\u0144stwa z ty\u0142u bramy.", "title": "Wprowad\u017a kod bezpiecze\u0144stwa" } - } + }, + "title": "IKEA TR\u00c5DFRI" } } \ No newline at end of file From 31dd327c59b659a505fa23fdf984e4c20d464294 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 2 Oct 2018 14:35:43 +0200 Subject: [PATCH 162/247] Catch possible errors from tradfri (#17068) * Catch possible errors from tradfri * Update config_flow.py --- homeassistant/components/tradfri/config_flow.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/tradfri/config_flow.py b/homeassistant/components/tradfri/config_flow.py index 29aa768dbb5..f3c52f42a9a 100644 --- a/homeassistant/components/tradfri/config_flow.py +++ b/homeassistant/components/tradfri/config_flow.py @@ -169,7 +169,9 @@ async def get_gateway_info(hass, host, identity, key): api = factory.request gateway = Gateway() gateway_info_result = await api(gateway.get_gateway_info()) - except RequestError: + except (OSError, RequestError): + # We're also catching OSError as PyTradfri doesn't catch that one yet + # Upstream PR: https://github.com/ggravlingen/pytradfri/pull/189 raise AuthError('cannot_connect') return { From 06340c98750435cef29d3aea1f1f4e8f8a792805 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Lov=C3=A9n?= Date: Tue, 2 Oct 2018 14:36:28 +0200 Subject: [PATCH 163/247] Allow no movement in vamera.onvif_ptz service (#17065) --- homeassistant/components/camera/onvif.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/camera/onvif.py b/homeassistant/components/camera/onvif.py index 9cf21dca9f9..2576dfa7f92 100644 --- a/homeassistant/components/camera/onvif.py +++ b/homeassistant/components/camera/onvif.py @@ -46,6 +46,7 @@ DIR_LEFT = "LEFT" DIR_RIGHT = "RIGHT" ZOOM_OUT = "ZOOM_OUT" ZOOM_IN = "ZOOM_IN" +PTZ_NONE = "NONE" SERVICE_PTZ = "onvif_ptz" @@ -65,9 +66,9 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ SERVICE_PTZ_SCHEMA = vol.Schema({ ATTR_ENTITY_ID: cv.entity_ids, - ATTR_PAN: vol.In([DIR_LEFT, DIR_RIGHT]), - ATTR_TILT: vol.In([DIR_UP, DIR_DOWN]), - ATTR_ZOOM: vol.In([ZOOM_OUT, ZOOM_IN]) + ATTR_PAN: vol.In([DIR_LEFT, DIR_RIGHT, PTZ_NONE]), + ATTR_TILT: vol.In([DIR_UP, DIR_DOWN, PTZ_NONE]), + ATTR_ZOOM: vol.In([ZOOM_OUT, ZOOM_IN, PTZ_NONE]) }) From 8e3a70e568e043ca9d19c1ce0d8e981cfa3aa6b7 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Tue, 2 Oct 2018 22:31:59 +0200 Subject: [PATCH 164/247] Upgrade youtube_dl to 2018.09.26 (#17079) --- 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 b3c02e5aee6..ffcb0f6ab95 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.09.18'] +REQUIREMENTS = ['youtube_dl==2018.09.26'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 84d432e582b..ad329545966 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1555,7 +1555,7 @@ yeelight==0.4.0 yeelightsunflower==0.0.10 # homeassistant.components.media_extractor -youtube_dl==2018.09.18 +youtube_dl==2018.09.26 # homeassistant.components.light.zengge zengge==0.2 From c78850a983d8111169d7147c79be26e38b02b586 Mon Sep 17 00:00:00 2001 From: Kevin Fronczak Date: Tue, 2 Oct 2018 22:17:14 -0400 Subject: [PATCH 165/247] Overhaul of Blink platform (#16942) * Using new methods for blink camera - Refactored blink platform (breaking change) - Camera needs to be uniquely enabled in config from now on - Added motion detection enable/disable to camera platform * Fix motion detection - bumped blinkpy to 0.8.1 - Added wifi strength sensor * Added platform schema to sensor - Added global variables for brand and attribution to main platform * Removed blink binary sensor * Add alarm control panel * Fixed dependency, added alarm_home * Update requirements * Fix lint errors * Updated throttle times * Add trigger_camera service (replaced snap_picture) * Add refresh after camera trigger * Update blinkpy version * Wait for valid camera response before returning image - Motion detection now working! * Updated for new blinkpy 0.9.0 * Add refresh control and other fixes for new blinkpy release * Add save video service * Pushing to force bot to update * Changed based on first review - Pass blink as BLINK_DATA instead of DOMAIN - Remove alarm_arm_home from alarm_control_panel - Re-add discovery with schema for sensors/binar_sensors - Change motion_detected to a binary_sensor - Added camera_armed binary sensor - Update camera device_state_attributes rather than state_attributes * Moved blink.py to own folder. Added service hints. * Updated coveragerc to reflect previous change * Register services with DOMAIN - Change device add for loop order in binary_sensor * Fix lint error * services.async_register -> services.register --- .coveragerc | 2 +- .../components/alarm_control_panel/blink.py | 86 ++++++++++ .../components/binary_sensor/blink.py | 55 ++---- homeassistant/components/blink.py | 89 ---------- homeassistant/components/blink/__init__.py | 161 ++++++++++++++++++ homeassistant/components/blink/services.yaml | 21 +++ homeassistant/components/camera/blink.py | 71 ++++---- homeassistant/components/sensor/blink.py | 56 +++--- requirements_all.txt | 2 +- 9 files changed, 346 insertions(+), 197 deletions(-) create mode 100644 homeassistant/components/alarm_control_panel/blink.py delete mode 100644 homeassistant/components/blink.py create mode 100644 homeassistant/components/blink/__init__.py create mode 100644 homeassistant/components/blink/services.yaml diff --git a/.coveragerc b/.coveragerc index fae3ebebbe7..fdd36f55925 100644 --- a/.coveragerc +++ b/.coveragerc @@ -56,7 +56,7 @@ omit = homeassistant/components/bbb_gpio.py homeassistant/components/*/bbb_gpio.py - homeassistant/components/blink.py + homeassistant/components/blink/* homeassistant/components/*/blink.py homeassistant/components/bloomsky.py diff --git a/homeassistant/components/alarm_control_panel/blink.py b/homeassistant/components/alarm_control_panel/blink.py new file mode 100644 index 00000000000..850ac52fda4 --- /dev/null +++ b/homeassistant/components/alarm_control_panel/blink.py @@ -0,0 +1,86 @@ +""" +Support for Blink Alarm Control Panel. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/alarm_control_panel.blink/ +""" +import logging + +from homeassistant.components.alarm_control_panel import AlarmControlPanel +from homeassistant.components.blink import ( + BLINK_DATA, DEFAULT_ATTRIBUTION) +from homeassistant.const import ( + ATTR_ATTRIBUTION, STATE_ALARM_DISARMED, STATE_ALARM_ARMED_AWAY) + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['blink'] + +ICON = 'mdi:security' + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Arlo Alarm Control Panels.""" + if discovery_info is None: + return + data = hass.data[BLINK_DATA] + + # Current version of blinkpy API only supports one sync module. When + # support for additional models is added, the sync module name should + # come from the API. + sync_modules = [] + sync_modules.append(BlinkSyncModule(data, 'sync')) + add_entities(sync_modules, True) + + +class BlinkSyncModule(AlarmControlPanel): + """Representation of a Blink Alarm Control Panel.""" + + def __init__(self, data, name): + """Initialize the alarm control panel.""" + self.data = data + self.sync = data.sync + self._name = name + self._state = None + + @property + def icon(self): + """Return icon.""" + return ICON + + @property + def state(self): + """Return the state of the device.""" + return self._state + + @property + def name(self): + """Return the name of the panel.""" + return "{} {}".format(BLINK_DATA, self._name) + + @property + def device_state_attributes(self): + """Return the state attributes.""" + return { + ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION, + } + + def update(self): + """Update the state of the device.""" + _LOGGER.debug("Updating Blink Alarm Control Panel %s", self._name) + self.data.refresh() + mode = self.sync.arm + if mode: + self._state = STATE_ALARM_ARMED_AWAY + else: + self._state = STATE_ALARM_DISARMED + + def alarm_disarm(self, code=None): + """Send disarm command.""" + self.sync.arm = False + self.sync.refresh() + + def alarm_arm_away(self, code=None): + """Send arm command.""" + self.sync.arm = True + self.sync.refresh() diff --git a/homeassistant/components/binary_sensor/blink.py b/homeassistant/components/binary_sensor/blink.py index 6ade20b72b9..6519d09a29a 100644 --- a/homeassistant/components/binary_sensor/blink.py +++ b/homeassistant/components/binary_sensor/blink.py @@ -2,10 +2,11 @@ Support for Blink system camera control. For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/binary_sensor.blink/ +https://home-assistant.io/components/binary_sensor.blink. """ -from homeassistant.components.blink import DOMAIN +from homeassistant.components.blink import BLINK_DATA, BINARY_SENSORS from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.const import CONF_MONITORED_CONDITIONS DEPENDENCIES = ['blink'] @@ -14,24 +15,27 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the blink binary sensors.""" if discovery_info is None: return + data = hass.data[BLINK_DATA] - data = hass.data[DOMAIN].blink - devs = list() - for name in data.cameras: - devs.append(BlinkCameraMotionSensor(name, data)) - devs.append(BlinkSystemSensor(data)) + devs = [] + for camera in data.sync.cameras: + for sensor_type in discovery_info[CONF_MONITORED_CONDITIONS]: + devs.append(BlinkBinarySensor(data, camera, sensor_type)) add_entities(devs, True) -class BlinkCameraMotionSensor(BinarySensorDevice): +class BlinkBinarySensor(BinarySensorDevice): """Representation of a Blink binary sensor.""" - def __init__(self, name, data): + def __init__(self, data, camera, sensor_type): """Initialize the sensor.""" - self._name = 'blink_' + name + '_motion_enabled' - self._camera_name = name self.data = data - self._state = self.data.cameras[self._camera_name].armed + self._type = sensor_type + name, icon = BINARY_SENSORS[sensor_type] + self._name = "{} {} {}".format(BLINK_DATA, camera, name) + self._icon = icon + self._camera = data.sync.cameras[camera] + self._state = None @property def name(self): @@ -46,29 +50,4 @@ class BlinkCameraMotionSensor(BinarySensorDevice): def update(self): """Update sensor state.""" self.data.refresh() - self._state = self.data.cameras[self._camera_name].armed - - -class BlinkSystemSensor(BinarySensorDevice): - """A representation of a Blink system sensor.""" - - def __init__(self, data): - """Initialize the sensor.""" - self._name = 'blink armed status' - self.data = data - self._state = self.data.arm - - @property - def name(self): - """Return the name of the blink sensor.""" - return self._name.replace(" ", "_") - - @property - def is_on(self): - """Return the status of the sensor.""" - return self._state - - def update(self): - """Update sensor state.""" - self.data.refresh() - self._state = self.data.arm + self._state = self._camera.attributes[self._type] diff --git a/homeassistant/components/blink.py b/homeassistant/components/blink.py deleted file mode 100644 index e84643711eb..00000000000 --- a/homeassistant/components/blink.py +++ /dev/null @@ -1,89 +0,0 @@ -""" -Support for Blink Home Camera System. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/blink/ -""" -import logging - -import voluptuous as vol - -import homeassistant.helpers.config_validation as cv -from homeassistant.const import ( - CONF_USERNAME, CONF_PASSWORD, ATTR_FRIENDLY_NAME, ATTR_ARMED) -from homeassistant.helpers import discovery - -REQUIREMENTS = ['blinkpy==0.6.0'] - -_LOGGER = logging.getLogger(__name__) - -DOMAIN = 'blink' - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string - }) -}, extra=vol.ALLOW_EXTRA) - -ARM_SYSTEM_SCHEMA = vol.Schema({ - vol.Optional(ATTR_ARMED): cv.boolean -}) - -ARM_CAMERA_SCHEMA = vol.Schema({ - vol.Required(ATTR_FRIENDLY_NAME): cv.string, - vol.Optional(ATTR_ARMED): cv.boolean -}) - -SNAP_PICTURE_SCHEMA = vol.Schema({ - vol.Required(ATTR_FRIENDLY_NAME): cv.string -}) - - -class BlinkSystem: - """Blink System class.""" - - def __init__(self, config_info): - """Initialize the system.""" - import blinkpy - self.blink = blinkpy.Blink(username=config_info[DOMAIN][CONF_USERNAME], - password=config_info[DOMAIN][CONF_PASSWORD]) - self.blink.setup_system() - - -def setup(hass, config): - """Set up Blink System.""" - hass.data[DOMAIN] = BlinkSystem(config) - discovery.load_platform(hass, 'camera', DOMAIN, {}, config) - discovery.load_platform(hass, 'sensor', DOMAIN, {}, config) - discovery.load_platform(hass, 'binary_sensor', DOMAIN, {}, config) - - def snap_picture(call): - """Take a picture.""" - cameras = hass.data[DOMAIN].blink.cameras - name = call.data.get(ATTR_FRIENDLY_NAME, '') - if name in cameras: - cameras[name].snap_picture() - - def arm_camera(call): - """Arm a camera.""" - cameras = hass.data[DOMAIN].blink.cameras - name = call.data.get(ATTR_FRIENDLY_NAME, '') - value = call.data.get(ATTR_ARMED, True) - if name in cameras: - cameras[name].set_motion_detect(value) - - def arm_system(call): - """Arm the system.""" - value = call.data.get(ATTR_ARMED, True) - hass.data[DOMAIN].blink.arm = value - hass.data[DOMAIN].blink.refresh() - - hass.services.register( - DOMAIN, 'snap_picture', snap_picture, schema=SNAP_PICTURE_SCHEMA) - hass.services.register( - DOMAIN, 'arm_camera', arm_camera, schema=ARM_CAMERA_SCHEMA) - hass.services.register( - DOMAIN, 'arm_system', arm_system, schema=ARM_SYSTEM_SCHEMA) - - return True diff --git a/homeassistant/components/blink/__init__.py b/homeassistant/components/blink/__init__.py new file mode 100644 index 00000000000..1d84b5be113 --- /dev/null +++ b/homeassistant/components/blink/__init__.py @@ -0,0 +1,161 @@ +""" +Support for Blink Home Camera System. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/blink/ +""" +import logging +from datetime import timedelta +import voluptuous as vol + +from homeassistant.helpers import ( + config_validation as cv, discovery) +from homeassistant.const import ( + CONF_USERNAME, CONF_PASSWORD, CONF_NAME, CONF_SCAN_INTERVAL, + CONF_BINARY_SENSORS, CONF_SENSORS, CONF_FILENAME, + CONF_MONITORED_CONDITIONS, TEMP_FAHRENHEIT) + +REQUIREMENTS = ['blinkpy==0.9.0'] + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = 'blink' +BLINK_DATA = 'blink' + +CONF_CAMERA = 'camera' +CONF_ALARM_CONTROL_PANEL = 'alarm_control_panel' + +DEFAULT_BRAND = 'Blink' +DEFAULT_ATTRIBUTION = "Data provided by immedia-semi.com" +SIGNAL_UPDATE_BLINK = "blink_update" + +DEFAULT_SCAN_INTERVAL = timedelta(seconds=60) + +TYPE_CAMERA_ARMED = 'motion_enabled' +TYPE_MOTION_DETECTED = 'motion_detected' +TYPE_TEMPERATURE = 'temperature' +TYPE_BATTERY = 'battery' +TYPE_WIFI_STRENGTH = 'wifi_strength' +TYPE_STATUS = 'status' + +SERVICE_REFRESH = 'blink_update' +SERVICE_TRIGGER = 'trigger_camera' +SERVICE_SAVE_VIDEO = 'save_video' + +BINARY_SENSORS = { + TYPE_CAMERA_ARMED: ['Camera Armed', 'mdi:verified'], + TYPE_MOTION_DETECTED: ['Motion Detected', 'mdi:run-fast'], +} + +SENSORS = { + TYPE_TEMPERATURE: ['Temperature', TEMP_FAHRENHEIT, 'mdi:thermometer'], + TYPE_BATTERY: ['Battery', '%', 'mdi:battery-80'], + TYPE_WIFI_STRENGTH: ['Wifi Signal', 'dBm', 'mdi:wifi-strength-2'], + TYPE_STATUS: ['Status', '', 'mdi:bell'] +} + +BINARY_SENSOR_SCHEMA = vol.Schema({ + vol.Optional(CONF_MONITORED_CONDITIONS, default=list(BINARY_SENSORS)): + vol.All(cv.ensure_list, [vol.In(BINARY_SENSORS)]) +}) + +SENSOR_SCHEMA = vol.Schema({ + vol.Optional(CONF_MONITORED_CONDITIONS, default=list(SENSORS)): + vol.All(cv.ensure_list, [vol.In(SENSORS)]) +}) + +SERVICE_TRIGGER_SCHEMA = vol.Schema({ + vol.Required(CONF_NAME): cv.string +}) + +SERVICE_SAVE_VIDEO_SCHEMA = vol.Schema({ + vol.Required(CONF_NAME): cv.string, + vol.Required(CONF_FILENAME): cv.string, +}) + +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: + vol.Schema({ + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL): + cv.time_period, + vol.Optional(CONF_BINARY_SENSORS, default={}): + BINARY_SENSOR_SCHEMA, + vol.Optional(CONF_SENSORS, default={}): SENSOR_SCHEMA, + }) + }, + extra=vol.ALLOW_EXTRA) + + +def setup(hass, config): + """Set up Blink System.""" + from blinkpy import blinkpy + conf = config[BLINK_DATA] + username = conf[CONF_USERNAME] + password = conf[CONF_PASSWORD] + scan_interval = conf[CONF_SCAN_INTERVAL] + hass.data[BLINK_DATA] = blinkpy.Blink(username=username, + password=password) + hass.data[BLINK_DATA].refresh_rate = scan_interval.total_seconds() + hass.data[BLINK_DATA].start() + + platforms = [ + ('alarm_control_panel', {}), + ('binary_sensor', conf[CONF_BINARY_SENSORS]), + ('camera', {}), + ('sensor', conf[CONF_SENSORS]), + ] + + for component, schema in platforms: + discovery.load_platform(hass, component, DOMAIN, schema, config) + + def trigger_camera(call): + """Trigger a camera.""" + cameras = hass.data[BLINK_DATA].sync.cameras + name = call.data[CONF_NAME] + if name in cameras: + cameras[name].snap_picture() + hass.data[BLINK_DATA].refresh(force_cache=True) + + def blink_refresh(event_time): + """Call blink to refresh info.""" + hass.data[BLINK_DATA].refresh(force_cache=True) + + async def async_save_video(call): + """Call save video service handler.""" + await async_handle_save_video_service(hass, call) + + hass.services.register(DOMAIN, SERVICE_REFRESH, blink_refresh) + hass.services.register(DOMAIN, + SERVICE_TRIGGER, + trigger_camera, + schema=SERVICE_TRIGGER_SCHEMA) + hass.services.register(DOMAIN, + SERVICE_SAVE_VIDEO, + async_save_video, + schema=SERVICE_SAVE_VIDEO_SCHEMA) + return True + + +async def async_handle_save_video_service(hass, call): + """Handle save video service calls.""" + camera_name = call.data[CONF_NAME] + video_path = call.data[CONF_FILENAME] + if not hass.config.is_allowed_path(video_path): + _LOGGER.error( + "Can't write %s, no access to path!", video_path) + return + + def _write_video(camera_name, video_path): + """Call video write.""" + all_cameras = hass.data[BLINK_DATA].sync.cameras + if camera_name in all_cameras: + all_cameras[camera_name].video_to_file(video_path) + + try: + await hass.async_add_executor_job( + _write_video, camera_name, video_path) + except OSError as err: + _LOGGER.error("Can't write image to file: %s", err) diff --git a/homeassistant/components/blink/services.yaml b/homeassistant/components/blink/services.yaml new file mode 100644 index 00000000000..fc042b0d598 --- /dev/null +++ b/homeassistant/components/blink/services.yaml @@ -0,0 +1,21 @@ +# Describes the format for available Blink services + +blink_update: + description: Force a refresh. + +trigger_camera: + description: Request named camera to take new image. + fields: + name: + description: Name of camera to take new image. + example: 'Living Room' + +save_video: + description: Save last recorded video clip to local file. + fields: + name: + description: Name of camera to grab video from. + example: 'Living Room' + filename: + description: Filename to writable path (directory may need to be included in whitelist_dirs in config) + example: '/tmp/video.mp4' diff --git a/homeassistant/components/camera/blink.py b/homeassistant/components/camera/blink.py index 217849138c3..5a728e92ce3 100644 --- a/homeassistant/components/camera/blink.py +++ b/homeassistant/components/camera/blink.py @@ -4,31 +4,27 @@ Support for Blink system camera. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/camera.blink/ """ -from datetime import timedelta import logging -import requests - -from homeassistant.components.blink import DOMAIN +from homeassistant.components.blink import BLINK_DATA, DEFAULT_BRAND from homeassistant.components.camera import Camera -from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['blink'] -MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=90) +ATTR_VIDEO_CLIP = 'video' +ATTR_IMAGE = 'image' def setup_platform(hass, config, add_entities, discovery_info=None): """Set up a Blink Camera.""" if discovery_info is None: return - - data = hass.data[DOMAIN].blink - devs = list() - for name in data.cameras: - devs.append(BlinkCamera(hass, config, data, name)) + data = hass.data[BLINK_DATA] + devs = [] + for name, camera in data.sync.cameras.items(): + devs.append(BlinkCamera(data, name, camera)) add_entities(devs) @@ -36,15 +32,15 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class BlinkCamera(Camera): """An implementation of a Blink Camera.""" - def __init__(self, hass, config, data, name): + def __init__(self, data, name, camera): """Initialize a camera.""" super().__init__() self.data = data - self.hass = hass - self._name = name - self.notifications = self.data.cameras[self._name].notifications + self._name = "{} {}".format(BLINK_DATA, name) + self._camera = camera self.response = None - + self.current_image = None + self.last_image = None _LOGGER.debug("Initialized blink camera %s", self._name) @property @@ -52,30 +48,29 @@ class BlinkCamera(Camera): """Return the camera name.""" return self._name - @Throttle(MIN_TIME_BETWEEN_UPDATES) - def request_image(self): - """Request a new image from Blink servers.""" - _LOGGER.debug("Requesting new image from blink servers") - image_url = self.check_for_motion() - header = self.data.cameras[self._name].header - self.response = requests.get(image_url, headers=header, stream=True) + @property + def device_state_attributes(self): + """Return the camera attributes.""" + return self._camera.attributes - def check_for_motion(self): - """Check if motion has been detected since last update.""" - self.data.refresh() - notifs = self.data.cameras[self._name].notifications - if notifs > self.notifications: - # We detected motion at some point - self.data.last_motion() - self.notifications = notifs - # Returning motion image currently not working - # return self.data.cameras[self._name].motion['image'] - elif notifs < self.notifications: - self.notifications = notifs + def enable_motion_detection(self): + """Enable motion detection for the camera.""" + self._camera.set_motion_detect(True) - return self.data.camera_thumbs[self._name] + def disable_motion_detection(self): + """Disable motion detection for the camera.""" + self._camera.set_motion_detect(False) + + @property + def motion_detection_enabled(self): + """Return the state of the camera.""" + return self._camera.armed + + @property + def brand(self): + """Return the camera brand.""" + return DEFAULT_BRAND def camera_image(self): """Return a still image response from the camera.""" - self.request_image() - return self.response.content + return self._camera.image_from_cache.content diff --git a/homeassistant/components/sensor/blink.py b/homeassistant/components/sensor/blink.py index 97356b6fc61..885bb939edf 100644 --- a/homeassistant/components/sensor/blink.py +++ b/homeassistant/components/sensor/blink.py @@ -6,34 +6,24 @@ https://home-assistant.io/components/sensor.blink/ """ import logging -from homeassistant.components.blink import DOMAIN -from homeassistant.const import TEMP_FAHRENHEIT +from homeassistant.components.blink import BLINK_DATA, SENSORS from homeassistant.helpers.entity import Entity +from homeassistant.const import CONF_MONITORED_CONDITIONS _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['blink'] -SENSOR_TYPES = { - 'temperature': ['Temperature', TEMP_FAHRENHEIT], - 'battery': ['Battery', ''], - 'notifications': ['Notifications', ''] -} - def setup_platform(hass, config, add_entities, discovery_info=None): """Set up a Blink sensor.""" if discovery_info is None: return - - data = hass.data[DOMAIN].blink - devs = list() - index = 0 - for name in data.cameras: - devs.append(BlinkSensor(name, 'temperature', index, data)) - devs.append(BlinkSensor(name, 'battery', index, data)) - devs.append(BlinkSensor(name, 'notifications', index, data)) - index += 1 + data = hass.data[BLINK_DATA] + devs = [] + for camera in data.sync.cameras: + for sensor_type in discovery_info[CONF_MONITORED_CONDITIONS]: + devs.append(BlinkSensor(data, camera, sensor_type)) add_entities(devs, True) @@ -41,21 +31,29 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class BlinkSensor(Entity): """A Blink camera sensor.""" - def __init__(self, name, sensor_type, index, data): + def __init__(self, data, camera, sensor_type): """Initialize sensors from Blink camera.""" - self._name = 'blink_' + name + '_' + SENSOR_TYPES[sensor_type][0] + name, units, icon = SENSORS[sensor_type] + self._name = "{} {} {}".format( + BLINK_DATA, camera, name) self._camera_name = name self._type = sensor_type self.data = data - self.index = index + self._camera = data.sync.cameras[camera] self._state = None - self._unit_of_measurement = SENSOR_TYPES[sensor_type][1] + self._unit_of_measurement = units + self._icon = icon @property def name(self): """Return the name of the camera.""" return self._name + @property + def icon(self): + """Return the icon of the sensor.""" + return self._icon + @property def state(self): """Return the camera's current state.""" @@ -68,13 +66,11 @@ class BlinkSensor(Entity): def update(self): """Retrieve sensor data from the camera.""" - camera = self.data.cameras[self._camera_name] - if self._type == 'temperature': - self._state = camera.temperature - elif self._type == 'battery': - self._state = camera.battery_string - elif self._type == 'notifications': - self._state = camera.notifications - else: + self.data.refresh() + try: + self._state = self._camera.attributes[self._type] + except KeyError: self._state = None - _LOGGER.warning("Could not retrieve state from %s", self.name) + _LOGGER.error( + "%s not a valid camera attribute. Did the API change?", + self._type) diff --git a/requirements_all.txt b/requirements_all.txt index ad329545966..47357131715 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -178,7 +178,7 @@ bellows==0.7.0 bimmer_connected==0.5.3 # homeassistant.components.blink -blinkpy==0.6.0 +blinkpy==0.9.0 # homeassistant.components.light.blinksticklight blinkstick==1.1.8 From 15a160a630641ea4ed1e98a8598058d1c407cb71 Mon Sep 17 00:00:00 2001 From: Dan Cinnamon Date: Tue, 2 Oct 2018 23:28:08 -0500 Subject: [PATCH 166/247] Bump pyenvisalink (#17086) --- homeassistant/components/envisalink.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/envisalink.py b/homeassistant/components/envisalink.py index eabe2d76851..e96810a8083 100644 --- a/homeassistant/components/envisalink.py +++ b/homeassistant/components/envisalink.py @@ -16,7 +16,7 @@ from homeassistant.helpers.entity import Entity from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.dispatcher import async_dispatcher_send -REQUIREMENTS = ['pyenvisalink==2.3'] +REQUIREMENTS = ['pyenvisalink==3.7'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 47357131715..bf76a78541d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -868,7 +868,7 @@ pyeight==0.0.9 pyemby==1.5 # homeassistant.components.envisalink -pyenvisalink==2.3 +pyenvisalink==3.7 # homeassistant.components.climate.ephember pyephember==0.2.0 From 4210835dcd7e9bf3f83db3b651d44f027e4917ea Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 3 Oct 2018 07:53:54 +0200 Subject: [PATCH 167/247] Async response all the things (#17073) * Use async_response * Update device_registry.py --- homeassistant/components/config/auth.py | 74 ++++++++----------- .../components/config/device_registry.py | 40 ++++------ .../components/websocket_api/__init__.py | 1 + .../components/websocket_api/decorators.py | 22 +++--- 4 files changed, 59 insertions(+), 78 deletions(-) diff --git a/homeassistant/components/config/auth.py b/homeassistant/components/config/auth.py index 6da0fe61d96..fb60b4075ef 100644 --- a/homeassistant/components/config/auth.py +++ b/homeassistant/components/config/auth.py @@ -1,9 +1,7 @@ """Offer API to configure Home Assistant auth.""" import voluptuous as vol -from homeassistant.core import callback from homeassistant.components import websocket_api -from homeassistant.components.websocket_api.decorators import require_owner WS_TYPE_LIST = 'config/auth/list' @@ -41,61 +39,49 @@ async def async_setup(hass): return True -@callback -@require_owner -def websocket_list(hass, connection, msg): +@websocket_api.require_owner +@websocket_api.async_response +async def websocket_list(hass, connection, msg): """Return a list of users.""" - async def send_users(): - """Send users.""" - result = [_user_info(u) for u in await hass.auth.async_get_users()] + result = [_user_info(u) for u in await hass.auth.async_get_users()] - connection.send_message( - websocket_api.result_message(msg['id'], result)) - - hass.async_create_task(send_users()) + connection.send_message( + websocket_api.result_message(msg['id'], result)) -@callback -@require_owner -def websocket_delete(hass, connection, msg): +@websocket_api.require_owner +@websocket_api.async_response +async def websocket_delete(hass, connection, msg): """Delete a user.""" - async def delete_user(): - """Delete user.""" - if msg['user_id'] == connection.user.id: - connection.send_message(websocket_api.error_message( - msg['id'], 'no_delete_self', - 'Unable to delete your own account')) - return + if msg['user_id'] == connection.user.id: + connection.send_message(websocket_api.error_message( + msg['id'], 'no_delete_self', + 'Unable to delete your own account')) + return - user = await hass.auth.async_get_user(msg['user_id']) + user = await hass.auth.async_get_user(msg['user_id']) - if not user: - connection.send_message(websocket_api.error_message( - msg['id'], 'not_found', 'User not found')) - return + if not user: + connection.send_message(websocket_api.error_message( + msg['id'], 'not_found', 'User not found')) + return - await hass.auth.async_remove_user(user) + await hass.auth.async_remove_user(user) - connection.send_message( - websocket_api.result_message(msg['id'])) - - hass.async_create_task(delete_user()) + connection.send_message( + websocket_api.result_message(msg['id'])) -@callback -@require_owner -def websocket_create(hass, connection, msg): +@websocket_api.require_owner +@websocket_api.async_response +async def websocket_create(hass, connection, msg): """Create a user.""" - async def create_user(): - """Create a user.""" - user = await hass.auth.async_create_user(msg['name']) + user = await hass.auth.async_create_user(msg['name']) - connection.send_message( - websocket_api.result_message(msg['id'], { - 'user': _user_info(user) - })) - - hass.async_create_task(create_user()) + connection.send_message( + websocket_api.result_message(msg['id'], { + 'user': _user_info(user) + })) def _user_info(user): diff --git a/homeassistant/components/config/device_registry.py b/homeassistant/components/config/device_registry.py index acffd516d21..ecbac703296 100644 --- a/homeassistant/components/config/device_registry.py +++ b/homeassistant/components/config/device_registry.py @@ -1,7 +1,6 @@ """HTTP views to interact with the device registry.""" import voluptuous as vol -from homeassistant.core import callback from homeassistant.helpers.device_registry import async_get_registry from homeassistant.components import websocket_api @@ -22,26 +21,19 @@ async def async_setup(hass): return True -@callback -def websocket_list_devices(hass, connection, msg): - """Handle list devices command. - - Async friendly. - """ - async def retrieve_entities(): - """Get devices from registry.""" - registry = await async_get_registry(hass) - connection.send_message(websocket_api.result_message( - msg['id'], [{ - 'config_entries': list(entry.config_entries), - 'connections': list(entry.connections), - 'manufacturer': entry.manufacturer, - 'model': entry.model, - 'name': entry.name, - 'sw_version': entry.sw_version, - 'id': entry.id, - 'hub_device_id': entry.hub_device_id, - } for entry in registry.devices.values()] - )) - - hass.async_create_task(retrieve_entities()) +@websocket_api.async_response +async def websocket_list_devices(hass, connection, msg): + """Handle list devices command.""" + registry = await async_get_registry(hass) + connection.send_message(websocket_api.result_message( + msg['id'], [{ + 'config_entries': list(entry.config_entries), + 'connections': list(entry.connections), + 'manufacturer': entry.manufacturer, + 'model': entry.model, + 'name': entry.name, + 'sw_version': entry.sw_version, + 'id': entry.id, + 'hub_device_id': entry.hub_device_id, + } for entry in registry.devices.values()] + )) diff --git a/homeassistant/components/websocket_api/__init__.py b/homeassistant/components/websocket_api/__init__.py index 41d0efaf3aa..90c802423ce 100644 --- a/homeassistant/components/websocket_api/__init__.py +++ b/homeassistant/components/websocket_api/__init__.py @@ -20,6 +20,7 @@ BASE_COMMAND_MESSAGE_SCHEMA = messages.BASE_COMMAND_MESSAGE_SCHEMA error_message = messages.error_message result_message = messages.result_message async_response = decorators.async_response +require_owner = decorators.require_owner ws_require_user = decorators.ws_require_user # pylint: enable=invalid-name diff --git a/homeassistant/components/websocket_api/decorators.py b/homeassistant/components/websocket_api/decorators.py index aaa054e4054..5f78790f5db 100644 --- a/homeassistant/components/websocket_api/decorators.py +++ b/homeassistant/components/websocket_api/decorators.py @@ -10,22 +10,24 @@ from . import messages _LOGGER = logging.getLogger(__name__) +async def _handle_async_response(func, hass, connection, msg): + """Create a response and handle exception.""" + try: + await func(hass, connection, msg) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + connection.send_message(messages.error_message( + msg['id'], 'unknown', 'Unexpected error occurred')) + + def async_response(func): """Decorate an async function to handle WebSocket API messages.""" - async def handle_msg_response(hass, connection, msg): - """Create a response and handle exception.""" - try: - await func(hass, connection, msg) - except Exception: # pylint: disable=broad-except - _LOGGER.exception("Unexpected exception") - connection.send_message(messages.error_message( - msg['id'], 'unknown', 'Unexpected error occurred')) - @callback @wraps(func) def schedule_handler(hass, connection, msg): """Schedule the handler.""" - hass.async_create_task(handle_msg_response(hass, connection, msg)) + hass.async_create_task( + _handle_async_response(func, hass, connection, msg)) return schedule_handler From 3cb20c7b4d074c39ad04fb01fb7f07f0eb77faea Mon Sep 17 00:00:00 2001 From: Steven Looman Date: Wed, 3 Oct 2018 11:07:25 +0200 Subject: [PATCH 168/247] Changes after review by @MartinHjelmare --- homeassistant/components/sensor/upnp.py | 5 +++-- homeassistant/components/upnp/__init__.py | 16 +++++++++++----- homeassistant/components/upnp/config_flow.py | 4 ++-- homeassistant/components/upnp/const.py | 1 + 4 files changed, 17 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/sensor/upnp.py b/homeassistant/components/sensor/upnp.py index e511b2947e5..c05e2ce0ade 100644 --- a/homeassistant/components/sensor/upnp.py +++ b/homeassistant/components/sensor/upnp.py @@ -4,7 +4,6 @@ Support for UPnP/IGD Sensors. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.upnp/ """ -# pylint: disable=invalid-name from datetime import datetime import logging @@ -12,6 +11,7 @@ from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity from homeassistant.components.upnp.const import DOMAIN as DATA_UPNP +from homeassistant.components.upnp.const import SIGNAL_REMOVE_SENSOR _LOGGER = logging.getLogger(__name__) @@ -88,9 +88,10 @@ class UpnpSensor(Entity): async def async_added_to_hass(self): """Subscribe to sensors events.""" async_dispatcher_connect(self.hass, - 'upnp_remove_sensor', + SIGNAL_REMOVE_SENSOR, self._upnp_remove_sensor) + @callback def _upnp_remove_sensor(self, device): """Remove sensor.""" if self._device != device: diff --git a/homeassistant/components/upnp/__init__.py b/homeassistant/components/upnp/__init__.py index 4ccb07af44b..265f68eaac0 100644 --- a/homeassistant/components/upnp/__init__.py +++ b/homeassistant/components/upnp/__init__.py @@ -4,7 +4,6 @@ Will open a port in your router for Home Assistant and provide statistics. For more details about this component, please refer to the documentation at https://home-assistant.io/components/upnp/ """ -# pylint: disable=invalid-name import asyncio from ipaddress import ip_address @@ -23,6 +22,7 @@ from .const import ( CONF_ENABLE_PORT_MAPPING, CONF_ENABLE_SENSORS, CONF_HASS, CONF_LOCAL_IP, CONF_PORTS, CONF_UDN, CONF_SSDP_DESCRIPTION, + SIGNAL_REMOVE_SENSOR, ) from .const import DOMAIN from .const import LOGGER as _LOGGER @@ -51,14 +51,20 @@ CONFIG_SCHEMA = vol.Schema({ def _substitute_hass_ports(ports, hass_port): - # substitute 'hass' for hass_port, both sides + """Substitute 'hass' for the hass_port.""" + ports = ports.copy() + + # substitute 'hass' for hass_port, both keys and values if CONF_HASS in ports: ports[hass_port] = ports[CONF_HASS] del ports[CONF_HASS] + for port in ports: if ports[port] == CONF_HASS: ports[port] = hass_port + return ports + # config async def async_setup(hass: HomeAssistantType, config: ConfigType): @@ -107,7 +113,7 @@ async def async_setup_entry(hass: HomeAssistantType, device = await Device.async_create_device(hass, ssdp_description) except (asyncio.TimeoutError, aiohttp.ClientError): _LOGGER.error('Unable to create upnp-device') - return + return False hass.data[DOMAIN]['devices'][device.udn] = device @@ -118,7 +124,7 @@ async def async_setup_entry(hass: HomeAssistantType, _LOGGER.debug('Enabling port mappings: %s', ports) hass_port = hass.http.server_port - _substitute_hass_ports(ports, hass_port) + ports = _substitute_hass_ports(ports, hass_port) await device.async_add_port_mappings(ports, local_ip=local_ip) # sensors @@ -155,7 +161,7 @@ async def async_unload_entry(hass: HomeAssistantType, # sensors if data.get(CONF_ENABLE_SENSORS): _LOGGER.debug('Deleting sensors') - dispatcher.async_dispatcher_send(hass, 'upnp_remove_sensor', device) + dispatcher.async_dispatcher_send(hass, SIGNAL_REMOVE_SENSOR, device) # clear stored device del hass.data[DOMAIN]['devices'][udn] diff --git a/homeassistant/components/upnp/config_flow.py b/homeassistant/components/upnp/config_flow.py index 65e2283115c..178faafb5f7 100644 --- a/homeassistant/components/upnp/config_flow.py +++ b/homeassistant/components/upnp/config_flow.py @@ -125,8 +125,8 @@ class UpnpFlowHandler(data_entry_flow.FlowHandler): data_schema=vol.Schema( OrderedDict([ (vol.Required('name'), vol.In(names)), - (vol.Optional('enable_sensors', default=False), bool), - (vol.Optional('enable_port_mapping', default=False), bool), + (vol.Optional('enable_sensors'), bool), + (vol.Optional('enable_port_mapping'), bool), ]) )) diff --git a/homeassistant/components/upnp/const.py b/homeassistant/components/upnp/const.py index ad57bc7d7f4..7a906ae02be 100644 --- a/homeassistant/components/upnp/const.py +++ b/homeassistant/components/upnp/const.py @@ -11,3 +11,4 @@ CONF_SSDP_DESCRIPTION = 'ssdp_description' CONF_UDN = 'udn' DOMAIN = 'upnp' LOGGER = logging.getLogger('homeassistant.components.upnp') +SIGNAL_REMOVE_SENSOR = 'upnp_remove_sensor' From 5d693277f02ccbd08e599cc004223a868f165289 Mon Sep 17 00:00:00 2001 From: Steven Looman Date: Wed, 3 Oct 2018 11:27:38 +0200 Subject: [PATCH 169/247] Fix stale docstrings --- homeassistant/components/upnp/__init__.py | 2 +- homeassistant/components/upnp/config_flow.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/upnp/__init__.py b/homeassistant/components/upnp/__init__.py index 265f68eaac0..f70fbcc4d20 100644 --- a/homeassistant/components/upnp/__init__.py +++ b/homeassistant/components/upnp/__init__.py @@ -103,7 +103,7 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType): # config flow async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigEntry): - """Set up a bridge from a config entry.""" + """Set up UPnP/IGD-device from a config entry.""" ensure_domain_data(hass) data = config_entry.data diff --git a/homeassistant/components/upnp/config_flow.py b/homeassistant/components/upnp/config_flow.py index 178faafb5f7..885f2f64211 100644 --- a/homeassistant/components/upnp/config_flow.py +++ b/homeassistant/components/upnp/config_flow.py @@ -28,7 +28,7 @@ def ensure_domain_data(hass): @config_entries.HANDLERS.register(DOMAIN) class UpnpFlowHandler(data_entry_flow.FlowHandler): - """Handle a Hue config flow.""" + """Handle a UPnP/IGD config flow.""" VERSION = 1 CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL From 2e5eb4d9dcc4c4f85d3fb8d11671a0b63084253f Mon Sep 17 00:00:00 2001 From: David Peterson Date: Wed, 3 Oct 2018 20:47:38 +1000 Subject: [PATCH 170/247] Add optional headers configuration for scrape (#17085) --- homeassistant/components/sensor/scrape.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/scrape.py b/homeassistant/components/sensor/scrape.py index 9a43c3ff295..0174407c7c3 100644 --- a/homeassistant/components/sensor/scrape.py +++ b/homeassistant/components/sensor/scrape.py @@ -13,7 +13,7 @@ from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.components.sensor.rest import RestData from homeassistant.const import ( CONF_NAME, CONF_RESOURCE, CONF_UNIT_OF_MEASUREMENT, STATE_UNKNOWN, - CONF_VALUE_TEMPLATE, CONF_VERIFY_SSL, CONF_USERNAME, + CONF_VALUE_TEMPLATE, CONF_VERIFY_SSL, CONF_USERNAME, CONF_HEADERS, CONF_PASSWORD, CONF_AUTHENTICATION, HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION) from homeassistant.helpers.entity import Entity @@ -35,6 +35,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_ATTR): cv.string, vol.Optional(CONF_AUTHENTICATION): vol.In([HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION]), + vol.Optional(CONF_HEADERS): vol.Schema({cv.string: cv.string}), vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_PASSWORD): cv.string, vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, @@ -49,7 +50,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None): name = config.get(CONF_NAME) resource = config.get(CONF_RESOURCE) method = 'GET' - payload = headers = None + payload = None + headers = config.get(CONF_HEADERS) verify_ssl = config.get(CONF_VERIFY_SSL) select = config.get(CONF_SELECT) attr = config.get(CONF_ATTR) From 704c9d8582e39101fc94f51058969b14d7cf6be6 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Wed, 3 Oct 2018 13:10:38 +0200 Subject: [PATCH 171/247] Add support for Hass.io discovery feature for Add-ons (#17035) * Update handler.py * Update __init__.py * Update handler.py * Update __init__.py * Create discovery.py * Update handler.py * Update discovery.py * Update __init__.py * Update discovery.py * Update discovery.py * Update discovery.py * Update struct * Update handler.py * Update discovery.py * Update discovery.py * Update discovery.py * Update __init__.py * Update discovery.py * Update discovery.py * Update discovery.py * Update discovery.py * Update discovery.py * Update discovery.py * Update discovery.py * Update discovery.py * Update __init__.py * Update discovery.py * fix lint * Update discovery.py * cleanup old discovery * Update discovery.py * Update discovery.py * Fix lint * Fix tests * Write more tests with new functions * Update test_handler.py * Create test_discovery.py * Update conftest.py * Update test_discovery.py * Update conftest.py * Update test_discovery.py * Update conftest.py * Update test_discovery.py * Update test_discovery.py * Update test_discovery.py * Update test_discovery.py * Update test_discovery.py * Fix test * Add test * fix lint * Update handler.py * Update discovery.py * Update test_discovery.py * fix lint * Lint --- homeassistant/components/hassio/__init__.py | 50 ++++---- homeassistant/components/hassio/discovery.py | 115 +++++++++++++++++++ homeassistant/components/hassio/handler.py | 44 ++++++- tests/components/hassio/conftest.py | 6 +- tests/components/hassio/test_discovery.py | 88 ++++++++++++++ tests/components/hassio/test_handler.py | 82 ++++++++----- 6 files changed, 326 insertions(+), 59 deletions(-) create mode 100644 homeassistant/components/hassio/discovery.py create mode 100644 tests/components/hassio/test_discovery.py diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index a0c603b018f..b97d748d864 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -19,7 +19,8 @@ import homeassistant.helpers.config_validation as cv from homeassistant.loader import bind_hass from homeassistant.util.dt import utcnow -from .handler import HassIO +from .handler import HassIO, HassioAPIError +from .discovery import async_setup_discovery from .http import HassIOView _LOGGER = logging.getLogger(__name__) @@ -136,10 +137,12 @@ def is_hassio(hass): async def async_check_config(hass): """Check configuration over Hass.io API.""" hassio = hass.data[DOMAIN] - result = await hassio.check_homeassistant_config() - if not result: - return "Hass.io config check API error" + try: + result = await hassio.check_homeassistant_config() + except HassioAPIError as err: + _LOGGER.error("Error on Hass.io API: %s", err) + if result['result'] == "error": return result['message'] return None @@ -147,18 +150,14 @@ async def async_check_config(hass): async def async_setup(hass, config): """Set up the Hass.io component.""" - try: - host = os.environ['HASSIO'] - except KeyError: - _LOGGER.error("Missing HASSIO environment variable.") - return False - - try: - os.environ['HASSIO_TOKEN'] - except KeyError: - _LOGGER.error("Missing HASSIO_TOKEN environment variable.") + # Check local setup + for env in ('HASSIO', 'HASSIO_TOKEN'): + if os.environ.get(env): + continue + _LOGGER.error("Missing %s environment variable.", env) return False + host = os.environ['HASSIO'] websession = hass.helpers.aiohttp_client.async_get_clientsession() hass.data[DOMAIN] = hassio = HassIO(hass.loop, websession, host) @@ -229,13 +228,13 @@ async def async_setup(hass, config): payload = data # Call API - ret = await hassio.send_command( - api_command.format(addon=addon, snapshot=snapshot), - payload=payload, timeout=MAP_SERVICE_API[service.service][2] - ) - - if not ret or ret['result'] != "ok": - _LOGGER.error("Error on Hass.io API: %s", ret['message']) + try: + await hassio.send_command( + api_command.format(addon=addon, snapshot=snapshot), + payload=payload, timeout=MAP_SERVICE_API[service.service][2] + ) + except HassioAPIError as err: + _LOGGER.error("Error on Hass.io API: %s", err) for service, settings in MAP_SERVICE_API.items(): hass.services.async_register( @@ -243,9 +242,11 @@ async def async_setup(hass, config): async def update_homeassistant_version(now): """Update last available Home Assistant version.""" - data = await hassio.get_homeassistant_info() - if data: + try: + data = await hassio.get_homeassistant_info() hass.data[DATA_HOMEASSISTANT_VERSION] = data['last_version'] + except HassioAPIError as err: + _LOGGER.warning("Can't read last version: %s", err) hass.helpers.event.async_track_point_in_utc_time( update_homeassistant_version, utcnow() + HASSIO_UPDATE_INTERVAL) @@ -276,4 +277,7 @@ async def async_setup(hass, config): hass.services.async_register( HASS_DOMAIN, service, async_handle_core_service) + # Init discovery Hass.io feature + async_setup_discovery(hass, hassio, config) + return True diff --git a/homeassistant/components/hassio/discovery.py b/homeassistant/components/hassio/discovery.py new file mode 100644 index 00000000000..ba5c8c3f789 --- /dev/null +++ b/homeassistant/components/hassio/discovery.py @@ -0,0 +1,115 @@ +"""Implement the serivces discovery feature from Hass.io for Add-ons.""" +import asyncio +import logging + +from aiohttp import web +from aiohttp.web_exceptions import HTTPServiceUnavailable + +from homeassistant.core import callback +from homeassistant.const import EVENT_HOMEASSISTANT_START +from homeassistant.components.http import HomeAssistantView + +from .handler import HassioAPIError + +_LOGGER = logging.getLogger(__name__) + +ATTR_DISCOVERY = 'discovery' +ATTR_ADDON = 'addon' +ATTR_NAME = 'name' +ATTR_SERVICE = 'service' +ATTR_CONFIG = 'config' +ATTR_UUID = 'uuid' + + +@callback +def async_setup_discovery(hass, hassio, config): + """Discovery setup.""" + hassio_discovery = HassIODiscovery(hass, hassio, config) + + # Handle exists discovery messages + async def async_discovery_start_handler(event): + """Process all exists discovery on startup.""" + try: + data = await hassio.retrieve_discovery_messages() + except HassioAPIError as err: + _LOGGER.error("Can't read discover info: %s", err) + return + + jobs = [hassio_discovery.async_process_new(discovery) + for discovery in data[ATTR_DISCOVERY]] + if jobs: + await asyncio.wait(jobs) + + hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_START, async_discovery_start_handler) + + hass.http.register_view(hassio_discovery) + + +class HassIODiscovery(HomeAssistantView): + """Hass.io view to handle base part.""" + + name = "api:hassio_push:discovery" + url = "/api/hassio_push/discovery/{uuid}" + + def __init__(self, hass, hassio, config): + """Initialize WebView.""" + self.hass = hass + self.hassio = hassio + self.config = config + + async def post(self, request, uuid): + """Handle new discovery requests.""" + # Fetch discovery data and prevent injections + try: + data = await self.hassio.get_discovery_message(uuid) + except HassioAPIError as err: + _LOGGER.error("Can't read discovey data: %s", err) + raise HTTPServiceUnavailable() from None + + await self.async_process_new(data) + return web.Response() + + async def delete(self, request, uuid): + """Handle remove discovery requests.""" + data = request.json() + + await self.async_process_del(data) + return web.Response() + + async def async_process_new(self, data): + """Process add discovery entry.""" + service = data[ATTR_SERVICE] + config_data = data[ATTR_CONFIG] + + # Read addinional Add-on info + try: + addon_info = await self.hassio.get_addon_info(data[ATTR_ADDON]) + except HassioAPIError as err: + _LOGGER.error("Can't read add-on info: %s", err) + return + config_data[ATTR_ADDON] = addon_info[ATTR_NAME] + + # Use config flow + await self.hass.config_entries.flow.async_init( + service, context={'source': 'hassio'}, data=config_data) + + async def async_process_del(self, data): + """Process remove discovery entry.""" + service = data[ATTR_SERVICE] + uuid = data[ATTR_UUID] + + # Check if realy deletet / prevent injections + try: + data = await self.hassio.get_discovery_message(uuid) + except HassioAPIError: + pass + else: + _LOGGER.warning("Retrieve wrong unload for %s", service) + return + + # Use config flow + for entry in self.hass.config_entries.async_entries(service): + if entry.source != 'hassio': + continue + await self.hass.config_entries.async_remove(entry) diff --git a/homeassistant/components/hassio/handler.py b/homeassistant/components/hassio/handler.py index bbf675ee47a..7c450b49bcc 100644 --- a/homeassistant/components/hassio/handler.py +++ b/homeassistant/components/hassio/handler.py @@ -21,12 +21,19 @@ _LOGGER = logging.getLogger(__name__) X_HASSIO = 'X-HASSIO-KEY' +class HassioAPIError(RuntimeError): + """Return if a API trow a error.""" + + def _api_bool(funct): """Return a boolean.""" async def _wrapper(*argv, **kwargs): """Wrap function.""" - data = await funct(*argv, **kwargs) - return data and data['result'] == "ok" + try: + data = await funct(*argv, **kwargs) + return data['result'] == "ok" + except HassioAPIError: + return False return _wrapper @@ -36,9 +43,9 @@ def _api_data(funct): async def _wrapper(*argv, **kwargs): """Wrap function.""" data = await funct(*argv, **kwargs) - if data and data['result'] == "ok": + if data['result'] == "ok": return data['data'] - return None + raise HassioAPIError(data['message']) return _wrapper @@ -68,6 +75,15 @@ class HassIO: """ return self.send_command("/homeassistant/info", method="get") + @_api_data + def get_addon_info(self, addon): + """Return data for a Add-on. + + This method return a coroutine. + """ + return self.send_command( + "/addons/{}/info".format(addon), method="get") + @_api_bool def restart_homeassistant(self): """Restart Home-Assistant container. @@ -91,6 +107,22 @@ class HassIO: """ return self.send_command("/homeassistant/check", timeout=300) + @_api_data + def retrieve_discovery_messages(self): + """Return all discovery data from Hass.io API. + + This method return a coroutine. + """ + return self.send_command("/discovery", method="get") + + @_api_data + def get_discovery_message(self, uuid): + """Return a single discovery data message. + + This method return a coroutine. + """ + return self.send_command("/discovery/{}".format(uuid), method="get") + @_api_bool async def update_hass_api(self, http_config, refresh_token): """Update Home Assistant API data on Hass.io.""" @@ -137,7 +169,7 @@ class HassIO: if request.status not in (200, 400): _LOGGER.error( "%s return code %d.", command, request.status) - return None + raise HassioAPIError() answer = await request.json() return answer @@ -148,4 +180,4 @@ class HassIO: except aiohttp.ClientError as err: _LOGGER.error("Client error on %s request %s", command, err) - return None + raise HassioAPIError() diff --git a/tests/components/hassio/conftest.py b/tests/components/hassio/conftest.py index 9f20efc08a5..fb3a172a45c 100644 --- a/tests/components/hassio/conftest.py +++ b/tests/components/hassio/conftest.py @@ -5,7 +5,7 @@ from unittest.mock import patch, Mock import pytest from homeassistant.setup import async_setup_component -from homeassistant.components.hassio.handler import HassIO +from homeassistant.components.hassio.handler import HassIO, HassioAPIError from tests.common import mock_coro from . import API_PASSWORD, HASSIO_TOKEN @@ -21,7 +21,7 @@ def hassio_env(): patch.dict(os.environ, {'HASSIO_TOKEN': "123456"}), \ patch('homeassistant.components.hassio.HassIO.' 'get_homeassistant_info', - Mock(return_value=mock_coro(None))): + Mock(side_effect=HassioAPIError())): yield @@ -32,7 +32,7 @@ def hassio_client(hassio_env, hass, aiohttp_client): Mock(return_value=mock_coro({"result": "ok"}))), \ patch('homeassistant.components.hassio.HassIO.' 'get_homeassistant_info', - Mock(return_value=mock_coro(None))): + Mock(side_effect=HassioAPIError())): hass.loop.run_until_complete(async_setup_component(hass, 'hassio', { 'http': { 'api_password': API_PASSWORD diff --git a/tests/components/hassio/test_discovery.py b/tests/components/hassio/test_discovery.py new file mode 100644 index 00000000000..98d0835c102 --- /dev/null +++ b/tests/components/hassio/test_discovery.py @@ -0,0 +1,88 @@ +"""Test config flow.""" +from unittest.mock import patch, Mock + +from homeassistant.const import EVENT_HOMEASSISTANT_START, HTTP_HEADER_HA_AUTH + +from tests.common import mock_coro +from . import API_PASSWORD + + +async def test_hassio_discovery_startup(hass, aioclient_mock, hassio_client): + """Test startup and discovery after event.""" + aioclient_mock.get( + "http://127.0.0.1/discovery", json={ + 'result': 'ok', 'data': {'discovery': [ + { + "service": "mqtt", "uuid": "test", + "addon": "mosquitto", "config": + { + 'broker': 'mock-broker', + 'port': 1883, + 'username': 'mock-user', + 'password': 'mock-pass', + 'protocol': '3.1.1' + } + } + ]}}) + aioclient_mock.get( + "http://127.0.0.1/addons/mosquitto/info", json={ + 'result': 'ok', 'data': {'name': "Mosquitto Test"} + }) + + with patch('homeassistant.components.mqtt.' + 'config_flow.FlowHandler.async_step_hassio', + Mock(return_value=mock_coro({"type": "abort"}))) as mock_mqtt: + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + + assert aioclient_mock.call_count == 2 + assert mock_mqtt.called + mock_mqtt.assert_called_with({ + 'broker': 'mock-broker', 'port': 1883, 'username': 'mock-user', + 'password': 'mock-pass', 'protocol': '3.1.1', + 'addon': 'Mosquitto Test', + }) + + +async def test_hassio_discovery_webhook(hass, aioclient_mock, hassio_client): + """Test discovery webhook.""" + aioclient_mock.get( + "http://127.0.0.1/discovery/testuuid", json={ + 'result': 'ok', 'data': + { + "service": "mqtt", "uuid": "test", + "addon": "mosquitto", "config": + { + 'broker': 'mock-broker', + 'port': 1883, + 'username': 'mock-user', + 'password': 'mock-pass', + 'protocol': '3.1.1' + } + } + }) + aioclient_mock.get( + "http://127.0.0.1/addons/mosquitto/info", json={ + 'result': 'ok', 'data': {'name': "Mosquitto Test"} + }) + + with patch('homeassistant.components.mqtt.' + 'config_flow.FlowHandler.async_step_hassio', + Mock(return_value=mock_coro({"type": "abort"}))) as mock_mqtt: + resp = await hassio_client.post( + '/api/hassio_push/discovery/testuuid', headers={ + HTTP_HEADER_HA_AUTH: API_PASSWORD + }, json={ + "addon": "mosquitto", "service": "mqtt", "uuid": "testuuid" + } + ) + await hass.async_block_till_done() + + assert resp.status == 200 + assert aioclient_mock.call_count == 2 + assert mock_mqtt.called + mock_mqtt.assert_called_with({ + 'broker': 'mock-broker', 'port': 1883, 'username': 'mock-user', + 'password': 'mock-pass', 'protocol': '3.1.1', + 'addon': 'Mosquitto Test', + }) diff --git a/tests/components/hassio/test_handler.py b/tests/components/hassio/test_handler.py index 78745489a78..db3917a2201 100644 --- a/tests/components/hassio/test_handler.py +++ b/tests/components/hassio/test_handler.py @@ -1,90 +1,118 @@ """The tests for the hassio component.""" -import asyncio import aiohttp +import pytest + +from homeassistant.components.hassio.handler import HassioAPIError -@asyncio.coroutine -def test_api_ping(hassio_handler, aioclient_mock): +async def test_api_ping(hassio_handler, aioclient_mock): """Test setup with API ping.""" aioclient_mock.get( "http://127.0.0.1/supervisor/ping", json={'result': 'ok'}) - assert (yield from hassio_handler.is_connected()) + assert (await hassio_handler.is_connected()) assert aioclient_mock.call_count == 1 -@asyncio.coroutine -def test_api_ping_error(hassio_handler, aioclient_mock): +async def test_api_ping_error(hassio_handler, aioclient_mock): """Test setup with API ping error.""" aioclient_mock.get( "http://127.0.0.1/supervisor/ping", json={'result': 'error'}) - assert not (yield from hassio_handler.is_connected()) + assert not (await hassio_handler.is_connected()) assert aioclient_mock.call_count == 1 -@asyncio.coroutine -def test_api_ping_exeption(hassio_handler, aioclient_mock): +async def test_api_ping_exeption(hassio_handler, aioclient_mock): """Test setup with API ping exception.""" aioclient_mock.get( "http://127.0.0.1/supervisor/ping", exc=aiohttp.ClientError()) - assert not (yield from hassio_handler.is_connected()) + assert not (await hassio_handler.is_connected()) assert aioclient_mock.call_count == 1 -@asyncio.coroutine -def test_api_homeassistant_info(hassio_handler, aioclient_mock): +async def test_api_homeassistant_info(hassio_handler, aioclient_mock): """Test setup with API homeassistant info.""" aioclient_mock.get( "http://127.0.0.1/homeassistant/info", json={ 'result': 'ok', 'data': {'last_version': '10.0'}}) - data = yield from hassio_handler.get_homeassistant_info() + data = await hassio_handler.get_homeassistant_info() assert aioclient_mock.call_count == 1 assert data['last_version'] == "10.0" -@asyncio.coroutine -def test_api_homeassistant_info_error(hassio_handler, aioclient_mock): +async def test_api_homeassistant_info_error(hassio_handler, aioclient_mock): """Test setup with API homeassistant info error.""" aioclient_mock.get( "http://127.0.0.1/homeassistant/info", json={ 'result': 'error', 'message': None}) - data = yield from hassio_handler.get_homeassistant_info() + with pytest.raises(HassioAPIError): + await hassio_handler.get_homeassistant_info() + assert aioclient_mock.call_count == 1 - assert data is None -@asyncio.coroutine -def test_api_homeassistant_stop(hassio_handler, aioclient_mock): +async def test_api_homeassistant_stop(hassio_handler, aioclient_mock): """Test setup with API HomeAssistant stop.""" aioclient_mock.post( "http://127.0.0.1/homeassistant/stop", json={'result': 'ok'}) - assert (yield from hassio_handler.stop_homeassistant()) + assert (await hassio_handler.stop_homeassistant()) assert aioclient_mock.call_count == 1 -@asyncio.coroutine -def test_api_homeassistant_restart(hassio_handler, aioclient_mock): +async def test_api_homeassistant_restart(hassio_handler, aioclient_mock): """Test setup with API HomeAssistant restart.""" aioclient_mock.post( "http://127.0.0.1/homeassistant/restart", json={'result': 'ok'}) - assert (yield from hassio_handler.restart_homeassistant()) + assert (await hassio_handler.restart_homeassistant()) assert aioclient_mock.call_count == 1 -@asyncio.coroutine -def test_api_homeassistant_config(hassio_handler, aioclient_mock): - """Test setup with API HomeAssistant restart.""" +async def test_api_homeassistant_config(hassio_handler, aioclient_mock): + """Test setup with API HomeAssistant config.""" aioclient_mock.post( "http://127.0.0.1/homeassistant/check", json={ 'result': 'ok', 'data': {'test': 'bla'}}) - data = yield from hassio_handler.check_homeassistant_config() + data = await hassio_handler.check_homeassistant_config() assert data['data']['test'] == 'bla' assert aioclient_mock.call_count == 1 + + +async def test_api_addon_info(hassio_handler, aioclient_mock): + """Test setup with API Add-on info.""" + aioclient_mock.get( + "http://127.0.0.1/addons/test/info", json={ + 'result': 'ok', 'data': {'name': 'bla'}}) + + data = await hassio_handler.get_addon_info("test") + assert data['name'] == 'bla' + assert aioclient_mock.call_count == 1 + + +async def test_api_discovery_message(hassio_handler, aioclient_mock): + """Test setup with API discovery message.""" + aioclient_mock.get( + "http://127.0.0.1/discovery/test", json={ + 'result': 'ok', 'data': {"service": "mqtt"}}) + + data = await hassio_handler.get_discovery_message("test") + assert data['service'] == "mqtt" + assert aioclient_mock.call_count == 1 + + +async def test_api_retrieve_discovery(hassio_handler, aioclient_mock): + """Test setup with API discovery message.""" + aioclient_mock.get( + "http://127.0.0.1/discovery", json={ + 'result': 'ok', 'data': {'discovery': [{"service": "mqtt"}]}}) + + data = await hassio_handler.retrieve_discovery_messages() + assert data['discovery'][-1]['service'] == "mqtt" + assert aioclient_mock.call_count == 1 From a5402739b705044ace7e40f02193bfeae1aa613a Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Wed, 3 Oct 2018 14:50:13 +0200 Subject: [PATCH 172/247] Keep the repeat mode when setting Sonos shuffle mode (#17083) * Keep the repeat mode when setting Sonos shuffle mode * Fix test --- homeassistant/components/media_player/sonos.py | 10 +++++----- tests/components/media_player/test_sonos.py | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/media_player/sonos.py b/homeassistant/components/media_player/sonos.py index 718db6300d0..b0c471aa602 100644 --- a/homeassistant/components/media_player/sonos.py +++ b/homeassistant/components/media_player/sonos.py @@ -341,7 +341,7 @@ class SonosDevice(MediaPlayerDevice): self._model = None self._player_volume = None self._player_muted = None - self._play_mode = None + self._shuffle = None self._name = None self._coordinator = None self._sonos_group = None @@ -437,7 +437,7 @@ class SonosDevice(MediaPlayerDevice): speaker_info = self.soco.get_speaker_info(True) self._name = speaker_info['zone_name'] self._model = speaker_info['model_name'] - self._play_mode = self.soco.play_mode + self._shuffle = self.soco.shuffle self.update_volume() @@ -530,7 +530,7 @@ class SonosDevice(MediaPlayerDevice): if new_status == 'TRANSITIONING': return - self._play_mode = self.soco.play_mode + self._shuffle = self.soco.shuffle if self.soco.is_playing_tv: self.update_media_linein(SOURCE_TV) @@ -755,7 +755,7 @@ class SonosDevice(MediaPlayerDevice): @soco_coordinator def shuffle(self): """Shuffling state.""" - return 'SHUFFLE' in self._play_mode + return self._shuffle @property def media_content_type(self): @@ -835,7 +835,7 @@ class SonosDevice(MediaPlayerDevice): @soco_coordinator def set_shuffle(self, shuffle): """Enable/Disable shuffle mode.""" - self.soco.play_mode = 'SHUFFLE_NOREPEAT' if shuffle else 'NORMAL' + self.soco.shuffle = shuffle @soco_error() def mute_volume(self, mute): diff --git a/tests/components/media_player/test_sonos.py b/tests/components/media_player/test_sonos.py index cb3da3ab899..cfe969a25c4 100644 --- a/tests/components/media_player/test_sonos.py +++ b/tests/components/media_player/test_sonos.py @@ -57,7 +57,7 @@ class SoCoMock(): self.is_visible = True self.volume = 50 self.mute = False - self.play_mode = 'NORMAL' + self.shuffle = False self.night_mode = False self.dialog_mode = False self.music_library = MusicLibraryMock() From 952a1b351311ac2d56afcc48bb6478e707528365 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Wed, 3 Oct 2018 15:09:05 +0200 Subject: [PATCH 173/247] Smaller steps for Sonos volume up/down (#17080) --- homeassistant/components/media_player/sonos.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/media_player/sonos.py b/homeassistant/components/media_player/sonos.py index b0c471aa602..51c77c5bb0e 100644 --- a/homeassistant/components/media_player/sonos.py +++ b/homeassistant/components/media_player/sonos.py @@ -335,7 +335,7 @@ class SonosDevice(MediaPlayerDevice): def __init__(self, player): """Initialize the Sonos device.""" self._receives_events = False - self._volume_increment = 5 + self._volume_increment = 2 self._unique_id = player.uid self._player = player self._model = None From 16cbc2d07fa543ef6612037bce8dc2dbfead69a0 Mon Sep 17 00:00:00 2001 From: David Bilay <32872847+dbilay@users.noreply.github.com> Date: Wed, 3 Oct 2018 16:15:45 +0200 Subject: [PATCH 174/247] Add weather condition code to OpenWeatherMap sensor (#17093) --- homeassistant/components/sensor/openweathermap.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/sensor/openweathermap.py b/homeassistant/components/sensor/openweathermap.py index 2dbbb581741..08426ed3eb8 100644 --- a/homeassistant/components/sensor/openweathermap.py +++ b/homeassistant/components/sensor/openweathermap.py @@ -39,6 +39,7 @@ SENSOR_TYPES = { 'clouds': ['Cloud coverage', '%'], 'rain': ['Rain', 'mm'], 'snow': ['Snow', 'mm'], + 'weather_code': ['Weather code', None], } PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ @@ -177,6 +178,8 @@ class OpenWeatherMapSensor(Entity): if fc_data is None: return self._state = fc_data.get_weathers()[0].get_detailed_status() + elif self.type == 'weather_code': + self._state = data.get_weather_code() class WeatherData: From da622abb79f8a8d43ed1173c527849a92971e1c7 Mon Sep 17 00:00:00 2001 From: Kevin Fronczak Date: Wed, 3 Oct 2018 11:04:31 -0400 Subject: [PATCH 175/247] Adding myself as blink codeowner (#17096) --- CODEOWNERS | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CODEOWNERS b/CODEOWNERS index 4b4019151b5..4c75e764b20 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -89,6 +89,8 @@ homeassistant/components/vacuum/roomba.py @pschmitt homeassistant/components/xiaomi_aqara.py @danielhiversen @syssi homeassistant/components/*/axis.py @kane610 +homeassistant/components/blink/* @fronzbot +homeassistant/components/*/blink.py @fronzbot homeassistant/components/*/bmw_connected_drive.py @ChristianKuehnel homeassistant/components/*/broadlink.py @danielhiversen homeassistant/components/*/deconz.py @kane610 From 467a59a6ed0efe434aa2c6e4b861f1a706e785ec Mon Sep 17 00:00:00 2001 From: Michael Wei Date: Wed, 3 Oct 2018 08:59:11 -0700 Subject: [PATCH 176/247] Change Tile icon to view-grid (#17098) --- homeassistant/components/device_tracker/tile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/device_tracker/tile.py b/homeassistant/components/device_tracker/tile.py index 07f15e7e88a..a18bd816a4b 100644 --- a/homeassistant/components/device_tracker/tile.py +++ b/homeassistant/components/device_tracker/tile.py @@ -32,7 +32,7 @@ ATTR_VOIP_STATE = 'voip_state' CONF_SHOW_INACTIVE = 'show_inactive' -DEFAULT_ICON = 'mdi:bluetooth' +DEFAULT_ICON = 'mdi:view-grid' DEFAULT_SCAN_INTERVAL = timedelta(minutes=2) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ From cf5f02b3475f89a2d90c6bfd2569357de6e7fe66 Mon Sep 17 00:00:00 2001 From: Tsvi Mostovicz Date: Wed, 3 Oct 2018 19:43:25 +0300 Subject: [PATCH 177/247] Fix jewish calendar sensor with language set to english (#17104) * Add failing testcase for issue #16830 * Fix for #16830 --- homeassistant/components/sensor/jewish_calendar.py | 8 ++++++-- tests/components/sensor/test_jewish_calendar.py | 12 ++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/jewish_calendar.py b/homeassistant/components/sensor/jewish_calendar.py index e5838fa8543..d78f007f22b 100644 --- a/homeassistant/components/sensor/jewish_calendar.py +++ b/homeassistant/components/sensor/jewish_calendar.py @@ -116,10 +116,14 @@ class JewishCalSensor(Entity): date.get_reading(self.diaspora), hebrew=self._hebrew) elif self.type == 'holiday_name': try: - self._state = next( - x.description[self._hebrew].long + description = next( + x.description[self._hebrew] for x in hdate.htables.HOLIDAYS if x.index == date.get_holyday()) + if not self._hebrew: + self._state = description + else: + self._state = description.long except StopIteration: self._state = None elif self.type == 'holyness': diff --git a/tests/components/sensor/test_jewish_calendar.py b/tests/components/sensor/test_jewish_calendar.py index 990f26d6ea7..e9a28f64cf9 100644 --- a/tests/components/sensor/test_jewish_calendar.py +++ b/tests/components/sensor/test_jewish_calendar.py @@ -95,6 +95,18 @@ class TestJewishCalenderSensor(unittest.TestCase): sensor.async_update(), self.hass.loop).result() self.assertEqual(sensor.state, "א\' ראש השנה") + def test_jewish_calendar_sensor_holiday_name_english(self): + """Test Jewish calendar sensor date output in hebrew.""" + test_time = dt(2018, 9, 10) + sensor = JewishCalSensor( + name='test', language='english', sensor_type='holiday_name', + latitude=self.TEST_LATITUDE, longitude=self.TEST_LONGITUDE, + diaspora=False) + with patch('homeassistant.util.dt.now', return_value=test_time): + run_coroutine_threadsafe( + sensor.async_update(), self.hass.loop).result() + self.assertEqual(sensor.state, "Rosh Hashana I") + def test_jewish_calendar_sensor_holyness(self): """Test Jewish calendar sensor date output in hebrew.""" test_time = dt(2018, 9, 10) From aeb21596a0acb032cf494893eb236bad44cdc006 Mon Sep 17 00:00:00 2001 From: mvn23 Date: Wed, 3 Oct 2018 23:12:21 +0200 Subject: [PATCH 178/247] Fix counter restore. (#17101) Add config option to disable restore (always use initial value on restart). Add unit tests for restore config option. --- homeassistant/components/counter/__init__.py | 20 ++++++----- tests/components/counter/test_init.py | 35 ++++++++++++++++++-- 2 files changed, 45 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/counter/__init__.py b/homeassistant/components/counter/__init__.py index 9ef4d4374ce..d67c93c0d6e 100644 --- a/homeassistant/components/counter/__init__.py +++ b/homeassistant/components/counter/__init__.py @@ -20,6 +20,7 @@ ATTR_INITIAL = 'initial' ATTR_STEP = 'step' CONF_INITIAL = 'initial' +CONF_RESTORE = 'restore' CONF_STEP = 'step' DEFAULT_INITIAL = 0 @@ -43,6 +44,7 @@ CONFIG_SCHEMA = vol.Schema({ vol.Optional(CONF_INITIAL, default=DEFAULT_INITIAL): cv.positive_int, vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_RESTORE, default=True): cv.boolean, vol.Optional(CONF_STEP, default=DEFAULT_STEP): cv.positive_int, }, None) }) @@ -61,10 +63,11 @@ async def async_setup(hass, config): name = cfg.get(CONF_NAME) initial = cfg.get(CONF_INITIAL) + restore = cfg.get(CONF_RESTORE) step = cfg.get(CONF_STEP) icon = cfg.get(CONF_ICON) - entities.append(Counter(object_id, name, initial, step, icon)) + entities.append(Counter(object_id, name, initial, restore, step, icon)) if not entities: return False @@ -86,10 +89,11 @@ async def async_setup(hass, config): class Counter(Entity): """Representation of a counter.""" - def __init__(self, object_id, name, initial, step, icon): + def __init__(self, object_id, name, initial, restore, step, icon): """Initialize a counter.""" self.entity_id = ENTITY_ID_FORMAT.format(object_id) self._name = name + self._restore = restore self._step = step self._state = self._initial = initial self._icon = icon @@ -124,12 +128,12 @@ class Counter(Entity): async def async_added_to_hass(self): """Call when entity about to be added to Home Assistant.""" - # If not None, we got an initial value. - if self._state is not None: - return - - state = await async_get_last_state(self.hass, self.entity_id) - self._state = state and state.state == state + # __init__ will set self._state to self._initial, only override + # if needed. + if self._restore: + state = await async_get_last_state(self.hass, self.entity_id) + if state is not None: + self._state = int(state.state) async def async_decrement(self): """Decrement the counter.""" diff --git a/tests/components/counter/test_init.py b/tests/components/counter/test_init.py index e5e0ee594ac..929d96d4650 100644 --- a/tests/components/counter/test_init.py +++ b/tests/components/counter/test_init.py @@ -7,7 +7,7 @@ import logging from homeassistant.core import CoreState, State, Context from homeassistant.setup import setup_component, async_setup_component from homeassistant.components.counter import ( - DOMAIN, CONF_INITIAL, CONF_STEP, CONF_NAME, CONF_ICON) + DOMAIN, CONF_INITIAL, CONF_RESTORE, CONF_STEP, CONF_NAME, CONF_ICON) from homeassistant.const import (ATTR_ICON, ATTR_FRIENDLY_NAME) from tests.common import (get_test_home_assistant, mock_restore_cache) @@ -55,6 +55,7 @@ class TestCounter(unittest.TestCase): CONF_NAME: 'Hello World', CONF_ICON: 'mdi:work', CONF_INITIAL: 10, + CONF_RESTORE: False, CONF_STEP: 5, } } @@ -172,9 +173,12 @@ def test_initial_state_overrules_restore_state(hass): yield from async_setup_component(hass, DOMAIN, { DOMAIN: { - 'test1': {}, + 'test1': { + CONF_RESTORE: False, + }, 'test2': { CONF_INITIAL: 10, + CONF_RESTORE: False, }, }}) @@ -187,6 +191,33 @@ def test_initial_state_overrules_restore_state(hass): assert int(state.state) == 10 +@asyncio.coroutine +def test_restore_state_overrules_initial_state(hass): + """Ensure states are restored on startup.""" + mock_restore_cache(hass, ( + State('counter.test1', '11'), + State('counter.test2', '-22'), + )) + + hass.state = CoreState.starting + + yield from async_setup_component(hass, DOMAIN, { + DOMAIN: { + 'test1': {}, + 'test2': { + CONF_INITIAL: 10, + }, + }}) + + state = hass.states.get('counter.test1') + assert state + assert int(state.state) == 11 + + state = hass.states.get('counter.test2') + assert state + assert int(state.state) == -22 + + @asyncio.coroutine def test_no_initial_state_and_no_restore_state(hass): """Ensure that entity is create without initial and restore feature.""" From 2b3019bdf475dc09478781ffb64de38b010dafa0 Mon Sep 17 00:00:00 2001 From: Michael Wei Date: Wed, 3 Oct 2018 16:22:21 -0700 Subject: [PATCH 179/247] Support multiple accounts in Tile, use device identifiers (#17108) * :sparkles: Update tile to support multiple accounts * :art: fix indent * :bug: fix format string * :art: use .format * :art: fix line indent --- homeassistant/components/device_tracker/tile.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/device_tracker/tile.py b/homeassistant/components/device_tracker/tile.py index a18bd816a4b..224aee4363b 100644 --- a/homeassistant/components/device_tracker/tile.py +++ b/homeassistant/components/device_tracker/tile.py @@ -29,6 +29,8 @@ ATTR_IS_DEAD = 'is_dead' ATTR_IS_LOST = 'is_lost' ATTR_RING_STATE = 'ring_state' ATTR_VOIP_STATE = 'voip_state' +ATTR_TILE_ID = 'tile_identifier' +ATTR_TILE_NAME = 'tile_name' CONF_SHOW_INACTIVE = 'show_inactive' @@ -50,8 +52,10 @@ async def async_setup_scanner(hass, config, async_see, discovery_info=None): websession = aiohttp_client.async_get_clientsession(hass) + config_file = hass.config.path(".{}{}".format( + slugify(config[CONF_USERNAME]), CLIENT_UUID_CONFIG_FILE)) config_data = await hass.async_add_job( - load_json, hass.config.path(CLIENT_UUID_CONFIG_FILE)) + load_json, config_file) if config_data: client = Client( config[CONF_USERNAME], @@ -63,10 +67,7 @@ async def async_setup_scanner(hass, config, async_see, discovery_info=None): config[CONF_USERNAME], config[CONF_PASSWORD], websession) config_data = {'client_uuid': client.client_uuid} - config_saved = await hass.async_add_job( - save_json, hass.config.path(CLIENT_UUID_CONFIG_FILE), config_data) - if not config_saved: - _LOGGER.error('Failed to save the client UUID') + await hass.async_add_job(save_json, config_file, config_data) scanner = TileScanner( client, hass, async_see, config[CONF_MONITORED_VARIABLES], @@ -125,7 +126,7 @@ class TileScanner: for tile in tiles: await self._async_see( - dev_id='tile_{0}'.format(slugify(tile['name'])), + dev_id='tile_{0}'.format(slugify(tile['tile_uuid'])), gps=( tile['tileState']['latitude'], tile['tileState']['longitude'] @@ -138,5 +139,7 @@ class TileScanner: ATTR_IS_LOST: tile['tileState']['is_lost'], ATTR_RING_STATE: tile['tileState']['ring_state'], ATTR_VOIP_STATE: tile['tileState']['voip_state'], + ATTR_TILE_ID: tile['tile_uuid'], + ATTR_TILE_NAME: tile['name'] }, icon=DEFAULT_ICON) From 04fdde0e862be54b06431e0502f19ba3bbba78d8 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Wed, 3 Oct 2018 21:03:29 -0600 Subject: [PATCH 180/247] Bumps simplisafe-python to 3.1.2 (#16931) * Bumps simplisafe-python to 3.0.4 * Updated requirements * Refresh token logic added * Member-requested changes * Removed unused import * Updated CODEOWNERS * Bump library to 3.1.2 --- CODEOWNERS | 1 + .../alarm_control_panel/simplisafe.py | 169 ++++++++++-------- requirements_all.txt | 2 +- 3 files changed, 100 insertions(+), 72 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 6736b3abd51..3bf22d52d67 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -41,6 +41,7 @@ homeassistant/components/hassio.py @home-assistant/hassio # Individual components homeassistant/components/alarm_control_panel/egardia.py @jeroenterheerdt homeassistant/components/alarm_control_panel/manual_mqtt.py @colinodell +homeassistant/components/alarm_control_panel/simplisafe.py @bachya homeassistant/components/binary_sensor/hikvision.py @mezz64 homeassistant/components/bmw_connected_drive.py @ChristianKuehnel homeassistant/components/camera/yi.py @bachya diff --git a/homeassistant/components/alarm_control_panel/simplisafe.py b/homeassistant/components/alarm_control_panel/simplisafe.py index 2c3b25330d9..34c68f26c2a 100644 --- a/homeassistant/components/alarm_control_panel/simplisafe.py +++ b/homeassistant/components/alarm_control_panel/simplisafe.py @@ -12,75 +12,100 @@ import voluptuous as vol from homeassistant.components.alarm_control_panel import ( PLATFORM_SCHEMA, AlarmControlPanel) from homeassistant.const import ( - CONF_CODE, CONF_NAME, CONF_PASSWORD, CONF_USERNAME, - STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, - STATE_ALARM_DISARMED, STATE_UNKNOWN) -import homeassistant.helpers.config_validation as cv + CONF_CODE, CONF_NAME, CONF_PASSWORD, CONF_USERNAME, STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED) +from homeassistant.helpers import aiohttp_client, config_validation as cv +from homeassistant.util.json import load_json, save_json -REQUIREMENTS = ['simplisafe-python==2.0.2'] +REQUIREMENTS = ['simplisafe-python==3.1.2'] _LOGGER = logging.getLogger(__name__) -DEFAULT_NAME = 'SimpliSafe' - ATTR_ALARM_ACTIVE = "alarm_active" ATTR_TEMPERATURE = "temperature" +DATA_FILE = '.simplisafe' + +DEFAULT_NAME = 'SimpliSafe' + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_PASSWORD): cv.string, vol.Required(CONF_USERNAME): cv.string, - vol.Optional(CONF_CODE): cv.string, + vol.Required(CONF_PASSWORD): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_CODE): 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 SimpliSafe platform.""" - from simplipy.api import SimpliSafeApiInterface, SimpliSafeAPIException + from simplipy import API + from simplipy.errors import SimplipyError + + username = config[CONF_USERNAME] + password = config[CONF_PASSWORD] name = config.get(CONF_NAME) code = config.get(CONF_CODE) - username = config.get(CONF_USERNAME) - password = config.get(CONF_PASSWORD) + + websession = aiohttp_client.async_get_clientsession(hass) + + config_data = await hass.async_add_executor_job( + load_json, hass.config.path(DATA_FILE)) try: - simplisafe = SimpliSafeApiInterface(username, password) - except SimpliSafeAPIException: - _LOGGER.error("Failed to set up SimpliSafe") + if config_data: + try: + simplisafe = await API.login_via_token( + config_data['refresh_token'], websession) + _LOGGER.debug('Logging in with refresh token') + except SimplipyError: + _LOGGER.info('Refresh token expired; attempting credentials') + simplisafe = await API.login_via_credentials( + username, password, websession) + else: + simplisafe = await API.login_via_credentials( + username, password, websession) + _LOGGER.debug('Logging in with credentials') + except SimplipyError as err: + _LOGGER.error("There was an error during setup: %s", err) return - systems = [] + config_data = {'refresh_token': simplisafe.refresh_token} + await hass.async_add_executor_job( + save_json, hass.config.path(DATA_FILE), config_data) - for system in simplisafe.get_systems(): - systems.append(SimpliSafeAlarm(system, name, code)) - - add_entities(systems) + systems = await simplisafe.get_systems() + async_add_entities( + [SimpliSafeAlarm(system, name, code) for system in systems], True) class SimpliSafeAlarm(AlarmControlPanel): """Representation of a SimpliSafe alarm.""" - def __init__(self, simplisafe, name, code): + def __init__(self, system, name, code): """Initialize the SimpliSafe alarm.""" - self.simplisafe = simplisafe - self._name = name + self._attrs = {} self._code = str(code) if code else None + self._name = name + self._system = system + self._state = None @property def unique_id(self): """Return the unique ID.""" - return self.simplisafe.location_id + return self._system.system_id @property def name(self): """Return the name of the device.""" - if self._name is not None: + if self._name: return self._name - return 'Alarm {}'.format(self.simplisafe.location_id) + return 'Alarm {}'.format(self._system.system_id) @property def code_format(self): """Return one or more digits/characters.""" - if self._code is None: + if not self._code: return None if isinstance(self._code, str) and re.search('^\\d+$', self._code): return 'Number' @@ -89,53 +114,12 @@ class SimpliSafeAlarm(AlarmControlPanel): @property def state(self): """Return the state of the device.""" - status = self.simplisafe.state - if status.lower() == 'off': - state = STATE_ALARM_DISARMED - elif status.lower() == 'home' or status.lower() == 'home_count': - state = STATE_ALARM_ARMED_HOME - elif (status.lower() == 'away' or status.lower() == 'exitDelay' or - status.lower() == 'away_count'): - state = STATE_ALARM_ARMED_AWAY - else: - state = STATE_UNKNOWN - return state + return self._state @property def device_state_attributes(self): """Return the state attributes.""" - attributes = {} - - attributes[ATTR_ALARM_ACTIVE] = self.simplisafe.alarm_active - if self.simplisafe.temperature is not None: - attributes[ATTR_TEMPERATURE] = self.simplisafe.temperature - - return attributes - - def update(self): - """Update alarm status.""" - self.simplisafe.update() - - def alarm_disarm(self, code=None): - """Send disarm command.""" - if not self._validate_code(code, 'disarming'): - return - self.simplisafe.set_state('off') - _LOGGER.info("SimpliSafe alarm disarming") - - def alarm_arm_home(self, code=None): - """Send arm home command.""" - if not self._validate_code(code, 'arming home'): - return - self.simplisafe.set_state('home') - _LOGGER.info("SimpliSafe alarm arming home") - - def alarm_arm_away(self, code=None): - """Send arm away command.""" - if not self._validate_code(code, 'arming away'): - return - self.simplisafe.set_state('away') - _LOGGER.info("SimpliSafe alarm arming away") + return self._attrs def _validate_code(self, code, state): """Validate given code.""" @@ -143,3 +127,46 @@ class SimpliSafeAlarm(AlarmControlPanel): if not check: _LOGGER.warning("Wrong code entered for %s", state) return check + + async def async_alarm_disarm(self, code=None): + """Send disarm command.""" + if not self._validate_code(code, 'disarming'): + return + + await self._system.set_off() + + async def async_alarm_arm_home(self, code=None): + """Send arm home command.""" + if not self._validate_code(code, 'arming home'): + return + + await self._system.set_home() + + async def async_alarm_arm_away(self, code=None): + """Send arm away command.""" + if not self._validate_code(code, 'arming away'): + return + + await self._system.set_away() + + async def async_update(self): + """Update alarm status.""" + await self._system.update() + + if self._system.state == self._system.SystemStates.off: + self._state = STATE_ALARM_DISARMED + elif self._system.state in ( + self._system.SystemStates.home, + self._system.SystemStates.home_count): + self._state = STATE_ALARM_ARMED_HOME + elif self._system.state in ( + self._system.SystemStates.away, + self._system.SystemStates.away_count, + self._system.SystemStates.exit_delay): + self._state = STATE_ALARM_ARMED_AWAY + else: + self._state = None + + self._attrs[ATTR_ALARM_ACTIVE] = self._system.alarm_going_off + if self._system.temperature: + self._attrs[ATTR_TEMPERATURE] = self._system.temperature diff --git a/requirements_all.txt b/requirements_all.txt index 0e610dcc521..4cb5097d69e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1335,7 +1335,7 @@ shodan==1.10.2 simplepush==1.1.4 # homeassistant.components.alarm_control_panel.simplisafe -simplisafe-python==2.0.2 +simplisafe-python==3.1.2 # homeassistant.components.sisyphus sisyphus-control==2.1 From abd329d7076b248c8fe127338f614a8b237e473d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Thu, 4 Oct 2018 10:03:31 +0300 Subject: [PATCH 181/247] Upgrade pytest to 3.8.2 (#17125) --- 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 87450f5422d..9f36d8f42ca 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -13,5 +13,5 @@ pytest-aiohttp==0.3.0 pytest-cov==2.5.1 pytest-sugar==0.9.1 pytest-timeout==1.3.2 -pytest==3.8.1 +pytest==3.8.2 requests_mock==1.5.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b5f83b5a55e..44d360f23ad 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -14,7 +14,7 @@ pytest-aiohttp==0.3.0 pytest-cov==2.5.1 pytest-sugar==0.9.1 pytest-timeout==1.3.2 -pytest==3.8.1 +pytest==3.8.2 requests_mock==1.5.2 From 6a0c9a718ea10118d9a0886e4a41cc87b1c2110a Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Thu, 4 Oct 2018 09:20:20 +0200 Subject: [PATCH 182/247] Fix sonos async use (#17099) * Entry setup wasn't using the async api. Fix this by using correct async api.s * Also use new async executor scheduler in async_added_to_hass. --- homeassistant/components/media_player/sonos.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/media_player/sonos.py b/homeassistant/components/media_player/sonos.py index 51c77c5bb0e..41ca1b4e85e 100644 --- a/homeassistant/components/media_player/sonos.py +++ b/homeassistant/components/media_player/sonos.py @@ -133,9 +133,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Sync version of async add devices.""" hass.add_job(async_add_entities, devices, update_before_add) - hass.add_job(_setup_platform, hass, - hass.data[SONOS_DOMAIN].get('media_player', {}), - add_entities, None) + hass.async_add_executor_job( + _setup_platform, hass, hass.data[SONOS_DOMAIN].get('media_player', {}), + add_entities, None) def _setup_platform(hass, config, add_entities, discovery_info): @@ -366,7 +366,7 @@ class SonosDevice(MediaPlayerDevice): async def async_added_to_hass(self): """Subscribe sonos events.""" self.hass.data[DATA_SONOS].devices.append(self) - self.hass.async_add_job(self._subscribe_to_player_events) + self.hass.async_add_executor_job(self._subscribe_to_player_events) @property def unique_id(self): From 3abdf217bb0f41d6f8164321e689bbc551b3ca6a Mon Sep 17 00:00:00 2001 From: Adam Mills Date: Thu, 4 Oct 2018 03:25:05 -0400 Subject: [PATCH 183/247] Homekit controller reconnect (#17060) * Add threaded call_later helper * Reconnect to device when connection fails * Consolidate connection logs and warn on first --- .../components/homekit_controller/__init__.py | 80 ++++++++++++++++--- homeassistant/helpers/event.py | 4 + tests/helpers/test_event.py | 19 ++++- 3 files changed, 90 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/homekit_controller/__init__.py b/homeassistant/components/homekit_controller/__init__.py index 5e24fe82340..5431dd4a61a 100644 --- a/homeassistant/components/homekit_controller/__init__.py +++ b/homeassistant/components/homekit_controller/__init__.py @@ -13,6 +13,7 @@ import uuid from homeassistant.components.discovery import SERVICE_HOMEKIT from homeassistant.helpers import discovery from homeassistant.helpers.entity import Entity +from homeassistant.helpers.event import call_later REQUIREMENTS = ['homekit==0.10'] @@ -37,6 +38,13 @@ KNOWN_DEVICES = "{}-devices".format(DOMAIN) _LOGGER = logging.getLogger(__name__) +REQUEST_TIMEOUT = 5 # seconds +RETRY_INTERVAL = 60 # seconds + + +class HomeKitConnectionError(ConnectionError): + """Raised when unable to connect to target device.""" + def homekit_http_send(self, message_body=None, encode_chunked=False): r"""Send the currently buffered request and clear the buffer. @@ -89,6 +97,9 @@ class HKDevice(): self.config_num = config_num self.config = config self.configurator = hass.components.configurator + self.conn = None + self.securecon = None + self._connection_warning_logged = False data_dir = os.path.join(hass.config.path(), HOMEKIT_DIR) if not os.path.isdir(data_dir): @@ -101,23 +112,35 @@ class HKDevice(): # pylint: disable=protected-access http.client.HTTPConnection._send_output = homekit_http_send - self.conn = http.client.HTTPConnection(self.host, port=self.port) if self.pairing_data is not None: self.accessory_setup() else: self.configure() + def connect(self): + """Open the connection to the HomeKit device.""" + # pylint: disable=import-error + import homekit + + self.conn = http.client.HTTPConnection( + self.host, port=self.port, timeout=REQUEST_TIMEOUT) + if self.pairing_data is not None: + controllerkey, accessorykey = \ + homekit.get_session_keys(self.conn, self.pairing_data) + self.securecon = homekit.SecureHttp( + self.conn.sock, accessorykey, controllerkey) + def accessory_setup(self): """Handle setup of a HomeKit accessory.""" # pylint: disable=import-error import homekit - self.controllerkey, self.accessorykey = \ - homekit.get_session_keys(self.conn, self.pairing_data) - self.securecon = homekit.SecureHttp(self.conn.sock, - self.accessorykey, - self.controllerkey) - response = self.securecon.get('/accessories') - data = json.loads(response.read().decode()) + + try: + data = self.get_json('/accessories') + except HomeKitConnectionError: + call_later( + self.hass, RETRY_INTERVAL, lambda _: self.accessory_setup()) + return for accessory in data['accessories']: serial = get_serial(accessory) if serial in self.hass.data[KNOWN_ACCESSORIES]: @@ -135,6 +158,31 @@ class HKDevice(): discovery.load_platform(self.hass, component, DOMAIN, service_info, self.config) + def get_json(self, target): + """Get JSON data from the device.""" + try: + if self.conn is None: + self.connect() + response = self.securecon.get(target) + data = json.loads(response.read().decode()) + + # After a successful connection, clear the warning logged status + self._connection_warning_logged = False + + return data + except (ConnectionError, OSError, json.JSONDecodeError) as ex: + # Mark connection as failed + if not self._connection_warning_logged: + _LOGGER.warning("Failed to connect to homekit device", + exc_info=ex) + self._connection_warning_logged = True + else: + _LOGGER.debug("Failed to connect to homekit device", + exc_info=ex) + self.conn = None + self.securecon = None + raise HomeKitConnectionError() from ex + def device_config_callback(self, callback_data): """Handle initial pairing.""" # pylint: disable=import-error @@ -142,6 +190,7 @@ class HKDevice(): pairing_id = str(uuid.uuid4()) code = callback_data.get('code').strip() try: + self.connect() self.pairing_data = homekit.perform_pair_setup(self.conn, code, pairing_id) except homekit.exception.UnavailableError: @@ -192,7 +241,7 @@ class HomeKitEntity(Entity): def __init__(self, accessory, devinfo): """Initialise a generic HomeKit device.""" self._name = accessory.model - self._securecon = accessory.securecon + self._accessory = accessory self._aid = devinfo['aid'] self._iid = devinfo['iid'] self._address = "homekit-{}-{}".format(devinfo['serial'], self._iid) @@ -201,8 +250,10 @@ class HomeKitEntity(Entity): def update(self): """Obtain a HomeKit device's state.""" - response = self._securecon.get('/accessories') - data = json.loads(response.read().decode()) + try: + data = self._accessory.get_json('/accessories') + except HomeKitConnectionError: + return for accessory in data['accessories']: if accessory['aid'] != self._aid: continue @@ -222,6 +273,11 @@ class HomeKitEntity(Entity): """Return the name of the device if any.""" return self._name + @property + def available(self) -> bool: + """Return True if entity is available.""" + return self._accessory.conn is not None + def update_characteristics(self, characteristics): """Synchronise a HomeKit device state with Home Assistant.""" raise NotImplementedError @@ -229,7 +285,7 @@ class HomeKitEntity(Entity): def put_characteristics(self, characteristics): """Control a HomeKit device state from Home Assistant.""" body = json.dumps({'characteristics': characteristics}) - self._securecon.put('/characteristics', body) + self._accessory.securecon.put('/characteristics', body) def setup(hass, config): diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index c8488fa3334..05555e8b5c6 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -227,6 +227,10 @@ def async_call_later(hass, delay, action): hass, action, dt_util.utcnow() + timedelta(seconds=delay)) +call_later = threaded_listener_factory( + async_call_later) + + @callback @bind_hass def async_track_time_interval(hass, action, interval): diff --git a/tests/helpers/test_event.py b/tests/helpers/test_event.py index deefcec773a..5b57ca75d51 100644 --- a/tests/helpers/test_event.py +++ b/tests/helpers/test_event.py @@ -13,6 +13,7 @@ import homeassistant.core as ha from homeassistant.const import MATCH_ALL from homeassistant.helpers.event import ( async_call_later, + call_later, track_point_in_utc_time, track_point_in_time, track_utc_time_change, @@ -645,6 +646,22 @@ class TestEventHelpers(unittest.TestCase): self.hass.block_till_done() self.assertEqual(0, len(specific_runs)) + def test_call_later(self): + """Test calling an action later.""" + def action(): pass + now = datetime(2017, 12, 19, 15, 40, 0, tzinfo=dt_util.UTC) + + with patch('homeassistant.helpers.event' + '.async_track_point_in_utc_time') as mock, \ + patch('homeassistant.util.dt.utcnow', return_value=now): + call_later(self.hass, 3, action) + + assert len(mock.mock_calls) == 1 + p_hass, p_action, p_point = mock.mock_calls[0][1] + assert p_hass is self.hass + assert p_action is action + assert p_point == now + timedelta(seconds=3) + @asyncio.coroutine def test_async_call_later(hass): @@ -659,7 +676,7 @@ def test_async_call_later(hass): assert len(mock.mock_calls) == 1 p_hass, p_action, p_point = mock.mock_calls[0][1] - assert hass is hass + assert p_hass is hass assert p_action is action assert p_point == now + timedelta(seconds=3) assert remove is mock() From 05d8c57212f3895a88d81de267e036923ddc35d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Thu, 4 Oct 2018 09:29:49 +0200 Subject: [PATCH 184/247] Tibber component and notify (#17062) * Refactor tibber, and Tibber notify * update Tibber lib. * tibber * Tibber coveragerc * Tibber upgrade lib * style * comments * use async_get_service * event --- .coveragerc | 4 +- homeassistant/components/notify/tibber.py | 37 ++++++++++++++ homeassistant/components/sensor/tibber.py | 24 +++------ homeassistant/components/tibber/__init__.py | 55 +++++++++++++++++++++ requirements_all.txt | 4 +- 5 files changed, 103 insertions(+), 21 deletions(-) create mode 100644 homeassistant/components/notify/tibber.py create mode 100644 homeassistant/components/tibber/__init__.py diff --git a/.coveragerc b/.coveragerc index 7941bd5708f..1cd4c1774e4 100644 --- a/.coveragerc +++ b/.coveragerc @@ -316,6 +316,9 @@ omit = homeassistant/components/*/thinkingcleaner.py + homeassistant/components/tibber/* + homeassistant/components/*/tibber.py + homeassistant/components/toon.py homeassistant/components/*/toon.py @@ -774,7 +777,6 @@ omit = homeassistant/components/sensor/tank_utility.py homeassistant/components/sensor/ted5000.py homeassistant/components/sensor/temper.py - homeassistant/components/sensor/tibber.py homeassistant/components/sensor/time_date.py homeassistant/components/sensor/torque.py homeassistant/components/sensor/trafikverket_weatherstation.py diff --git a/homeassistant/components/notify/tibber.py b/homeassistant/components/notify/tibber.py new file mode 100644 index 00000000000..ddbcb3f6c65 --- /dev/null +++ b/homeassistant/components/notify/tibber.py @@ -0,0 +1,37 @@ +""" +Tibber platform for notify component. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/notify.tibber/ +""" +import asyncio +import logging + +from homeassistant.components.notify import ( + ATTR_TITLE, ATTR_TITLE_DEFAULT, BaseNotificationService) +from homeassistant.components.tibber import DOMAIN as TIBBER_DOMAIN + + +_LOGGER = logging.getLogger(__name__) + + +async def async_get_service(hass, config, discovery_info=None): + """Get the Tibber notification service.""" + tibber_connection = hass.data[TIBBER_DOMAIN] + return TibberNotificationService(tibber_connection.send_notification) + + +class TibberNotificationService(BaseNotificationService): + """Implement the notification service for Tibber.""" + + def __init__(self, notify): + """Initialize the service.""" + self._notify = notify + + async def async_send_message(self, message=None, **kwargs): + """Send a message to Tibber devices.""" + title = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT) + try: + await self._notify(title=title, message=message) + except asyncio.TimeoutError: + _LOGGER.error("Timeout sending message with Tibber") diff --git a/homeassistant/components/sensor/tibber.py b/homeassistant/components/sensor/tibber.py index 0f127d7dd64..1207c8dfe20 100644 --- a/homeassistant/components/sensor/tibber.py +++ b/homeassistant/components/sensor/tibber.py @@ -10,25 +10,15 @@ import logging from datetime import timedelta import aiohttp -import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import EVENT_HOMEASSISTANT_STOP, CONF_ACCESS_TOKEN +from homeassistant.components.tibber import DOMAIN as TIBBER_DOMAIN from homeassistant.exceptions import PlatformNotReady -from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.entity import Entity from homeassistant.util import dt as dt_util from homeassistant.util import Throttle -REQUIREMENTS = ['pyTibber==0.6.0'] - _LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_ACCESS_TOKEN): cv.string -}) - ICON = 'mdi:currency-usd' ICON_RT = 'mdi:power-plug' SCAN_INTERVAL = timedelta(minutes=1) @@ -38,16 +28,14 @@ MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5) async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Tibber sensor.""" - import tibber - tibber_connection = tibber.Tibber(config[CONF_ACCESS_TOKEN], - websession=async_get_clientsession(hass)) + if discovery_info is None: + _LOGGER.error("Tibber sensor configuration has changed." + " Check https://home-assistant.io/components/tibber/") + return - async def _close(*_): - await tibber_connection.rt_disconnect() - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _close) + tibber_connection = hass.data.get(TIBBER_DOMAIN) try: - await tibber_connection.update_info() dev = [] for home in tibber_connection.get_homes(): await home.update_info() diff --git a/homeassistant/components/tibber/__init__.py b/homeassistant/components/tibber/__init__.py new file mode 100644 index 00000000000..8022902c580 --- /dev/null +++ b/homeassistant/components/tibber/__init__.py @@ -0,0 +1,55 @@ +""" +Support for Tibber. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/tibber/ +""" +import asyncio +import logging + +import aiohttp +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.const import (EVENT_HOMEASSISTANT_STOP, CONF_ACCESS_TOKEN, + CONF_NAME) +from homeassistant.helpers import discovery +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +REQUIREMENTS = ['pyTibber==0.7.2'] + +DOMAIN = 'tibber' + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_ACCESS_TOKEN): cv.string, + }) +}, extra=vol.ALLOW_EXTRA) + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup(hass, config): + """Set up the Tibber component.""" + conf = config.get(DOMAIN) + + import tibber + tibber_connection = tibber.Tibber(conf[CONF_ACCESS_TOKEN], + websession=async_get_clientsession(hass)) + hass.data[DOMAIN] = tibber_connection + + async def _close(event): + await tibber_connection.rt_disconnect() + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _close) + + try: + await tibber_connection.update_info() + except (asyncio.TimeoutError, aiohttp.ClientError): + return False + + for component in ['sensor', 'notify']: + discovery.load_platform(hass, component, DOMAIN, + {CONF_NAME: DOMAIN}, config) + + return True diff --git a/requirements_all.txt b/requirements_all.txt index 4cb5097d69e..ead19a57f17 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -767,8 +767,8 @@ pyRFXtrx==0.23 # homeassistant.components.switch.switchmate pySwitchmate==0.4.1 -# homeassistant.components.sensor.tibber -pyTibber==0.6.0 +# homeassistant.components.tibber +pyTibber==0.7.2 # homeassistant.components.switch.dlink pyW215==0.6.0 From cc1891ef2b2afdeac5c31a2fd93d2b18a0fe7445 Mon Sep 17 00:00:00 2001 From: Jerad Meisner Date: Thu, 4 Oct 2018 01:24:14 -0700 Subject: [PATCH 185/247] Add time created to persistent notifications. (#17121) * Add time created to persistent notifications. * UTC --- .../components/persistent_notification/__init__.py | 8 ++++++-- tests/components/persistent_notification/test_init.py | 2 ++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/persistent_notification/__init__.py b/homeassistant/components/persistent_notification/__init__.py index 9c09ccced3d..d38501b9b07 100644 --- a/homeassistant/components/persistent_notification/__init__.py +++ b/homeassistant/components/persistent_notification/__init__.py @@ -17,7 +17,9 @@ from homeassistant.loader import bind_hass from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.util import slugify +import homeassistant.util.dt as dt_util +ATTR_CREATED_AT = 'created_at' ATTR_MESSAGE = 'message' ATTR_NOTIFICATION_ID = 'notification_id' ATTR_TITLE = 'title' @@ -147,6 +149,7 @@ async def async_setup(hass: HomeAssistant, config: dict) -> Awaitable[bool]: ATTR_NOTIFICATION_ID: notification_id, ATTR_STATUS: STATUS_UNREAD, ATTR_TITLE: title, + ATTR_CREATED_AT: dt_util.utcnow(), } hass.bus.async_fire(EVENT_PERSISTENT_NOTIFICATIONS_UPDATED) @@ -203,8 +206,9 @@ def websocket_get_notifications( connection.send_message( websocket_api.result_message(msg['id'], [ { - key: data[key] for key in (ATTR_NOTIFICATION_ID, ATTR_MESSAGE, - ATTR_STATUS, ATTR_TITLE) + key: data[key] for key in (ATTR_NOTIFICATION_ID, + ATTR_MESSAGE, ATTR_STATUS, + ATTR_TITLE, ATTR_CREATED_AT) } for data in hass.data[DOMAIN]['notifications'].values() ]) diff --git a/tests/components/persistent_notification/test_init.py b/tests/components/persistent_notification/test_init.py index 5df106a5327..43b91e2622a 100644 --- a/tests/components/persistent_notification/test_init.py +++ b/tests/components/persistent_notification/test_init.py @@ -41,6 +41,7 @@ class TestPersistentNotification: assert notification['status'] == pn.STATUS_UNREAD assert notification['message'] == 'Hello World 2' assert notification['title'] == '2 beers' + assert notification['created_at'] is not None notifications.clear() def test_create_notification_id(self): @@ -174,6 +175,7 @@ async def test_ws_get_notifications(hass, hass_ws_client): assert notification['message'] == 'test' assert notification['title'] is None assert notification['status'] == pn.STATUS_UNREAD + assert notification['created_at'] is not None # Mark Read await hass.services.async_call(pn.DOMAIN, pn.SERVICE_MARK_READ, { From a559c06d6bcd50ba89585791fb70a2f564aa6560 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 4 Oct 2018 10:41:13 +0200 Subject: [PATCH 186/247] Make it easier for auth to consume newer formats (#17127) --- homeassistant/auth/auth_store.py | 24 +++++++++++++++++++----- homeassistant/auth/models.py | 18 ++++++++---------- homeassistant/helpers/storage.py | 4 ++-- tests/common.py | 2 +- 4 files changed, 30 insertions(+), 18 deletions(-) diff --git a/homeassistant/auth/auth_store.py b/homeassistant/auth/auth_store.py index c170344746e..54c34d8ec2c 100644 --- a/homeassistant/auth/auth_store.py +++ b/homeassistant/auth/auth_store.py @@ -1,9 +1,9 @@ """Storage for auth models.""" from collections import OrderedDict from datetime import timedelta +import hmac from logging import getLogger from typing import Any, Dict, List, Optional # noqa: F401 -import hmac from homeassistant.auth.const import ACCESS_TOKEN_EXPIRATION from homeassistant.core import HomeAssistant, callback @@ -214,14 +214,24 @@ class AuthStore: if self._users is not None: return - users = OrderedDict() # type: Dict[str, models.User] - if data is None: - self._users = users + self._set_defaults() return + users = OrderedDict() # type: Dict[str, models.User] + + # When creating objects we mention each attribute explicetely. This + # prevents crashing if user rolls back HA version after a new property + # was added. + for user_dict in data['users']: - users[user_dict['id']] = models.User(**user_dict) + users[user_dict['id']] = models.User( + name=user_dict['name'], + id=user_dict['id'], + is_owner=user_dict['is_owner'], + is_active=user_dict['is_active'], + system_generated=user_dict['system_generated'], + ) for cred_dict in data['credentials']: users[cred_dict['user_id']].credentials.append(models.Credentials( @@ -341,3 +351,7 @@ class AuthStore: 'credentials': credentials, 'refresh_tokens': refresh_tokens, } + + def _set_defaults(self) -> None: + """Set default values for auth store.""" + self._users = OrderedDict() # type: Dict[str, models.User] diff --git a/homeassistant/auth/models.py b/homeassistant/auth/models.py index b0f4024c3ab..bd00ca72b83 100644 --- a/homeassistant/auth/models.py +++ b/homeassistant/auth/models.py @@ -19,19 +19,19 @@ class User: """A user.""" name = attr.ib(type=str) # type: Optional[str] - id = attr.ib(type=str, default=attr.Factory(lambda: uuid.uuid4().hex)) + id = attr.ib(type=str, factory=lambda: uuid.uuid4().hex) is_owner = attr.ib(type=bool, default=False) is_active = attr.ib(type=bool, default=False) system_generated = attr.ib(type=bool, default=False) # List of credentials of a user. credentials = attr.ib( - type=list, default=attr.Factory(list), cmp=False + type=list, factory=list, cmp=False ) # type: List[Credentials] # Tokens associated with a user. refresh_tokens = attr.ib( - type=dict, default=attr.Factory(dict), cmp=False + type=dict, factory=dict, cmp=False ) # type: Dict[str, RefreshToken] @@ -48,12 +48,10 @@ class RefreshToken: validator=attr.validators.in_(( TOKEN_TYPE_NORMAL, TOKEN_TYPE_SYSTEM, TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN))) - id = attr.ib(type=str, default=attr.Factory(lambda: uuid.uuid4().hex)) - created_at = attr.ib(type=datetime, default=attr.Factory(dt_util.utcnow)) - token = attr.ib(type=str, - default=attr.Factory(lambda: generate_secret(64))) - jwt_key = attr.ib(type=str, - default=attr.Factory(lambda: generate_secret(64))) + id = attr.ib(type=str, factory=lambda: uuid.uuid4().hex) + created_at = attr.ib(type=datetime, factory=dt_util.utcnow) + token = attr.ib(type=str, factory=lambda: generate_secret(64)) + jwt_key = attr.ib(type=str, factory=lambda: generate_secret(64)) last_used_at = attr.ib(type=Optional[datetime], default=None) last_used_ip = attr.ib(type=Optional[str], default=None) @@ -69,7 +67,7 @@ class Credentials: # Allow the auth provider to store data to represent their auth. data = attr.ib(type=dict) - id = attr.ib(type=str, default=attr.Factory(lambda: uuid.uuid4().hex)) + id = attr.ib(type=str, factory=lambda: uuid.uuid4().hex) is_new = attr.ib(type=bool, default=True) diff --git a/homeassistant/helpers/storage.py b/homeassistant/helpers/storage.py index 7d867b50ec2..cfe73d6d147 100644 --- a/homeassistant/helpers/storage.py +++ b/homeassistant/helpers/storage.py @@ -2,7 +2,7 @@ import asyncio import logging import os -from typing import Dict, Optional, Callable +from typing import Dict, Optional, Callable, Any from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import callback @@ -63,7 +63,7 @@ class Store: """Return the config path.""" return self.hass.config.path(STORAGE_DIR, self.key) - async def async_load(self): + async def async_load(self) -> Optional[Dict[str, Any]]: """Load data. If the expected version does not match the given version, the migrate diff --git a/tests/common.py b/tests/common.py index 0cb15d683b5..ee181cfa2e9 100644 --- a/tests/common.py +++ b/tests/common.py @@ -392,7 +392,7 @@ def ensure_auth_manager_loaded(auth_mgr): """Ensure an auth manager is considered loaded.""" store = auth_mgr._store if store._users is None: - store._users = OrderedDict() + store._set_defaults() class MockModule: From 178bf736f6c0f5a349af7f7d036fbd8764942d2e Mon Sep 17 00:00:00 2001 From: Heiko Thiery Date: Thu, 4 Oct 2018 12:16:27 +0200 Subject: [PATCH 187/247] Add new component fritzbox binary_sensor (#17057) * Add new component fritzbox binary_sensor Signed-off-by: Heiko Thiery * fix failed flake8 test Signed-off-by: Heiko Thiery * add new file to .coveragerc Signed-off-by: Heiko Thiery * use wildcard to cover all platforms Signed-off-by: Heiko Thiery * remove polling because polling is true by default Signed-off-by: Heiko Thiery * add blank line to keep imports ordered Signed-off-by: Heiko Thiery * Minor changes --- .coveragerc | 2 +- .../components/binary_sensor/fritzbox.py | 64 +++++++++++++++++++ homeassistant/components/fritzbox.py | 2 +- 3 files changed, 66 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/binary_sensor/fritzbox.py diff --git a/.coveragerc b/.coveragerc index 1cd4c1774e4..378dba5ef2a 100644 --- a/.coveragerc +++ b/.coveragerc @@ -112,7 +112,7 @@ omit = homeassistant/components/*/evohome.py homeassistant/components/fritzbox.py - homeassistant/components/switch/fritzbox.py + homeassistant/components/*/fritzbox.py homeassistant/components/ecovacs.py homeassistant/components/*/ecovacs.py diff --git a/homeassistant/components/binary_sensor/fritzbox.py b/homeassistant/components/binary_sensor/fritzbox.py new file mode 100644 index 00000000000..ab58e6e84bc --- /dev/null +++ b/homeassistant/components/binary_sensor/fritzbox.py @@ -0,0 +1,64 @@ +""" +Support for Fritzbox binary sensors. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/binary_sensor.fritzbox/ +""" +import logging + +import requests + +from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.fritzbox import DOMAIN as FRITZBOX_DOMAIN + +DEPENDENCIES = ['fritzbox'] + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Fritzbox binary sensor platform.""" + devices = [] + fritz_list = hass.data[FRITZBOX_DOMAIN] + + for fritz in fritz_list: + device_list = fritz.get_devices() + for device in device_list: + if device.has_alarm: + devices.append(FritzboxBinarySensor(device, fritz)) + + add_entities(devices, True) + + +class FritzboxBinarySensor(BinarySensorDevice): + """Representation of a binary Fritzbox device.""" + + def __init__(self, device, fritz): + """Initialize the Fritzbox binary sensor.""" + self._device = device + self._fritz = fritz + + @property + def name(self): + """Return the name of the entity.""" + return self._device.name + + @property + def device_class(self): + """Return the class of this sensor.""" + return 'window' + + @property + def is_on(self): + """Return true if sensor is on.""" + if not self._device.present: + return False + return self._device.alert_state + + def update(self): + """Get latest data from the Fritzbox.""" + try: + self._device.update() + except requests.exceptions.HTTPError as ex: + _LOGGER.warning("Connection error: %s", ex) + self._fritz.login() diff --git a/homeassistant/components/fritzbox.py b/homeassistant/components/fritzbox.py index 49bc4c8f6e6..e6f121799df 100644 --- a/homeassistant/components/fritzbox.py +++ b/homeassistant/components/fritzbox.py @@ -18,7 +18,7 @@ _LOGGER = logging.getLogger(__name__) REQUIREMENTS = ['pyfritzhome==0.4.0'] -SUPPORTED_DOMAINS = ['climate', 'switch'] +SUPPORTED_DOMAINS = ['binary_sensor', 'climate', 'switch'] DOMAIN = 'fritzbox' From 0d93bf9a456024c7d57388f25c69d2558f23caad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Thu, 4 Oct 2018 13:24:54 +0200 Subject: [PATCH 188/247] Update xiaomi lib (#17129) --- homeassistant/components/xiaomi_aqara.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/xiaomi_aqara.py b/homeassistant/components/xiaomi_aqara.py index 790510db3d3..27414a64150 100644 --- a/homeassistant/components/xiaomi_aqara.py +++ b/homeassistant/components/xiaomi_aqara.py @@ -22,7 +22,7 @@ from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.util.dt import utcnow from homeassistant.util import slugify -REQUIREMENTS = ['PyXiaomiGateway==0.10.0'] +REQUIREMENTS = ['PyXiaomiGateway==0.11.0'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index ead19a57f17..82a0571b001 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -58,7 +58,7 @@ PyRMVtransport==0.1 PySwitchbot==0.3 # homeassistant.components.xiaomi_aqara -PyXiaomiGateway==0.10.0 +PyXiaomiGateway==0.11.0 # homeassistant.components.rpi_gpio # RPi.GPIO==0.6.1 From ae0dd33e2ec2c17800137f96a985e3cc4c16ab50 Mon Sep 17 00:00:00 2001 From: Alok Saboo Date: Thu, 4 Oct 2018 08:02:30 -0400 Subject: [PATCH 189/247] Add update service to Google Travel Sensor (#17092) * Add update service * Update service name * Register service only once * Make hound happy --- .../components/sensor/google_travel_time.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/google_travel_time.py b/homeassistant/components/sensor/google_travel_time.py index a69b865f30b..6c197475653 100644 --- a/homeassistant/components/sensor/google_travel_time.py +++ b/homeassistant/components/sensor/google_travel_time.py @@ -10,7 +10,7 @@ import logging import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import DOMAIN, PLATFORM_SCHEMA from homeassistant.helpers.entity import Entity from homeassistant.const import ( CONF_API_KEY, CONF_NAME, EVENT_HOMEASSISTANT_START, ATTR_LATITUDE, @@ -68,6 +68,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) TRACKABLE_DOMAINS = ['device_tracker', 'sensor', 'zone'] +DATA_KEY = 'google_travel_time' def convert_time_to_utc(timestr): @@ -90,6 +91,10 @@ def setup_platform(hass, config, add_entities_callback, discovery_info=None): if options.get('units') is None: options['units'] = hass.config.units.name + if DATA_KEY not in hass.data: + hass.data[DATA_KEY] = [] + hass.services.register( + DOMAIN, 'google_travel_sensor_update', update) travel_mode = config.get(CONF_TRAVEL_MODE) mode = options.get(CONF_MODE) @@ -110,10 +115,19 @@ def setup_platform(hass, config, add_entities_callback, discovery_info=None): sensor = GoogleTravelTimeSensor( hass, name, api_key, origin, destination, options) + hass.data[DATA_KEY].append(sensor) if sensor.valid_api_connection: add_entities_callback([sensor]) + def update(service): + """Update service for manual updates.""" + entity_id = service.data.get('entity_id') + for sensor in hass.data[DATA_KEY]: + if sensor.entity_id == entity_id: + sensor.update(no_throttle=True) + sensor.schedule_update_ha_state() + # Wait until start event is sent to load this component. hass.bus.listen_once(EVENT_HOMEASSISTANT_START, run_setup) From b61a250321cc58024512175edc03c61120cc024d Mon Sep 17 00:00:00 2001 From: Jason Hu Date: Thu, 4 Oct 2018 06:50:42 -0700 Subject: [PATCH 190/247] Fix upnp component l10n error (#17132) --- .../components/upnp/.translations/en.json | 26 +++++++++---------- homeassistant/components/upnp/strings.json | 2 -- 2 files changed, 12 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/upnp/.translations/en.json b/homeassistant/components/upnp/.translations/en.json index 682150b7ddd..93e1db62f8e 100644 --- a/homeassistant/components/upnp/.translations/en.json +++ b/homeassistant/components/upnp/.translations/en.json @@ -1,25 +1,23 @@ { "config": { - "title": "UPnP/IGD", + "abort": { + "already_configured": "UPnP/IGD is already configured", + "no_devices_discovered": "No UPnP/IGDs discovered", + "no_sensors_or_port_mapping": "Enable at least sensors or port mapping" + }, "step": { "init": { "title": "UPnP/IGD" }, "user": { - "title": "Configuration options for the UPnP/IGD", - "data":{ - "igd": "UPnP/IGD", + "data": { + "enable_port_mapping": "Enable port mapping for Home Assistant", "enable_sensors": "Add traffic sensors", - "enable_port_mapping": "Enable port mapping for Home Assistant" - } + "igd": "UPnP/IGD" + }, + "title": "Configuration options for the UPnP/IGD" } }, - "error": { - }, - "abort": { - "no_devices_discovered": "No UPnP/IGDs discovered", - "already_configured": "UPnP/IGD is already configured", - "no_sensors_or_port_mapping": "Enable at least sensors or port mapping" - } + "title": "UPnP/IGD" } -} +} \ No newline at end of file diff --git a/homeassistant/components/upnp/strings.json b/homeassistant/components/upnp/strings.json index 682150b7ddd..9dd4c3f5ad0 100644 --- a/homeassistant/components/upnp/strings.json +++ b/homeassistant/components/upnp/strings.json @@ -14,8 +14,6 @@ } } }, - "error": { - }, "abort": { "no_devices_discovered": "No UPnP/IGDs discovered", "already_configured": "UPnP/IGD is already configured", From c9976718d4edafc7acbb099dc86226d9cb442253 Mon Sep 17 00:00:00 2001 From: Tsvi Mostovicz Date: Thu, 4 Oct 2018 16:51:56 +0300 Subject: [PATCH 191/247] Take timezone into consideration when calulating Zmanim. Partial fix for #16946 (#17131) --- .../components/sensor/jewish_calendar.py | 9 +++-- .../components/sensor/test_jewish_calendar.py | 37 ++++++++++++++++--- 2 files changed, 37 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/sensor/jewish_calendar.py b/homeassistant/components/sensor/jewish_calendar.py index d78f007f22b..1de45d6145e 100644 --- a/homeassistant/components/sensor/jewish_calendar.py +++ b/homeassistant/components/sensor/jewish_calendar.py @@ -64,7 +64,8 @@ async def async_setup_platform( dev = [] for sensor_type in config[CONF_SENSORS]: dev.append(JewishCalSensor( - name, language, sensor_type, latitude, longitude, diaspora)) + name, language, sensor_type, latitude, longitude, + hass.config.time_zone, diaspora)) async_add_entities(dev, True) @@ -72,7 +73,8 @@ class JewishCalSensor(Entity): """Representation of an Jewish calendar sensor.""" def __init__( - self, name, language, sensor_type, latitude, longitude, diaspora): + self, name, language, sensor_type, latitude, longitude, timezone, + diaspora): """Initialize the Jewish calendar sensor.""" self.client_name = name self._name = SENSOR_TYPES[sensor_type][0] @@ -81,6 +83,7 @@ class JewishCalSensor(Entity): self._state = None self.latitude = latitude self.longitude = longitude + self.timezone = timezone self.diaspora = diaspora _LOGGER.debug("Sensor %s initialized", self.type) @@ -131,7 +134,7 @@ class JewishCalSensor(Entity): else: times = hdate.Zmanim( date=today, latitude=self.latitude, longitude=self.longitude, - hebrew=self._hebrew).zmanim + timezone=self.timezone, hebrew=self._hebrew).zmanim self._state = times[self.type].time() _LOGGER.debug("New value: %s", self._state) diff --git a/tests/components/sensor/test_jewish_calendar.py b/tests/components/sensor/test_jewish_calendar.py index e9a28f64cf9..b67e340a9aa 100644 --- a/tests/components/sensor/test_jewish_calendar.py +++ b/tests/components/sensor/test_jewish_calendar.py @@ -1,5 +1,6 @@ """The tests for the Jewish calendar sensor platform.""" import unittest +from datetime import time from datetime import datetime as dt from unittest.mock import patch @@ -64,7 +65,7 @@ class TestJewishCalenderSensor(unittest.TestCase): sensor = JewishCalSensor( name='test', language='english', sensor_type='date', latitude=self.TEST_LATITUDE, longitude=self.TEST_LONGITUDE, - diaspora=False) + timezone="UTC", diaspora=False) with patch('homeassistant.util.dt.now', return_value=test_time): run_coroutine_threadsafe( sensor.async_update(), @@ -77,7 +78,7 @@ class TestJewishCalenderSensor(unittest.TestCase): sensor = JewishCalSensor( name='test', language='hebrew', sensor_type='date', latitude=self.TEST_LATITUDE, longitude=self.TEST_LONGITUDE, - diaspora=False) + timezone="UTC", diaspora=False) with patch('homeassistant.util.dt.now', return_value=test_time): run_coroutine_threadsafe( sensor.async_update(), self.hass.loop).result() @@ -89,7 +90,7 @@ class TestJewishCalenderSensor(unittest.TestCase): sensor = JewishCalSensor( name='test', language='hebrew', sensor_type='holiday_name', latitude=self.TEST_LATITUDE, longitude=self.TEST_LONGITUDE, - diaspora=False) + timezone="UTC", diaspora=False) with patch('homeassistant.util.dt.now', return_value=test_time): run_coroutine_threadsafe( sensor.async_update(), self.hass.loop).result() @@ -101,7 +102,7 @@ class TestJewishCalenderSensor(unittest.TestCase): sensor = JewishCalSensor( name='test', language='english', sensor_type='holiday_name', latitude=self.TEST_LATITUDE, longitude=self.TEST_LONGITUDE, - diaspora=False) + timezone="UTC", diaspora=False) with patch('homeassistant.util.dt.now', return_value=test_time): run_coroutine_threadsafe( sensor.async_update(), self.hass.loop).result() @@ -113,7 +114,7 @@ class TestJewishCalenderSensor(unittest.TestCase): sensor = JewishCalSensor( name='test', language='hebrew', sensor_type='holyness', latitude=self.TEST_LATITUDE, longitude=self.TEST_LONGITUDE, - diaspora=False) + timezone="UTC", diaspora=False) with patch('homeassistant.util.dt.now', return_value=test_time): run_coroutine_threadsafe( sensor.async_update(), self.hass.loop).result() @@ -125,8 +126,32 @@ class TestJewishCalenderSensor(unittest.TestCase): sensor = JewishCalSensor( name='test', language='hebrew', sensor_type='weekly_portion', latitude=self.TEST_LATITUDE, longitude=self.TEST_LONGITUDE, - diaspora=False) + timezone="UTC", diaspora=False) with patch('homeassistant.util.dt.now', return_value=test_time): run_coroutine_threadsafe( sensor.async_update(), self.hass.loop).result() self.assertEqual(sensor.state, "פרשת נצבים") + + def test_jewish_calendar_sensor_first_stars_ny(self): + """Test Jewish calendar sensor date output in hebrew.""" + test_time = dt(2018, 9, 8) + sensor = JewishCalSensor( + name='test', language='hebrew', sensor_type='first_stars', + latitude=40.7128, longitude=-74.0060, + timezone="America/New_York", diaspora=False) + with patch('homeassistant.util.dt.now', return_value=test_time): + run_coroutine_threadsafe( + sensor.async_update(), self.hass.loop).result() + self.assertEqual(sensor.state, time(19, 48)) + + def test_jewish_calendar_sensor_first_stars_jerusalem(self): + """Test Jewish calendar sensor date output in hebrew.""" + test_time = dt(2018, 9, 8) + sensor = JewishCalSensor( + name='test', language='hebrew', sensor_type='first_stars', + latitude=self.TEST_LATITUDE, longitude=self.TEST_LONGITUDE, + timezone="Asia/Jerusalem", diaspora=False) + with patch('homeassistant.util.dt.now', return_value=test_time): + run_coroutine_threadsafe( + sensor.async_update(), self.hass.loop).result() + self.assertEqual(sensor.state, time(19, 21)) From 0cfbb9ce91888801a96d4a8131a0ed96e5a7f925 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 4 Oct 2018 15:53:50 +0200 Subject: [PATCH 192/247] Allow config entry setup to raise not ready (#17135) --- homeassistant/config_entries.py | 59 +++++++++++++++++++++++++-------- homeassistant/exceptions.py | 6 ++++ tests/test_config_entries.py | 49 ++++++++++++++++++++++++++- 3 files changed, 100 insertions(+), 14 deletions(-) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index f3649aca453..1cc2e1362af 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -115,14 +115,13 @@ should follow the same return values as a normal step. If the result of the step is to show a form, the user will be able to continue the flow from the config panel. """ - import logging import uuid from typing import Set, Optional, List, Dict # noqa pylint: disable=unused-import from homeassistant import data_entry_flow from homeassistant.core import callback, HomeAssistant -from homeassistant.exceptions import HomeAssistantError +from homeassistant.exceptions import HomeAssistantError, ConfigEntryNotReady from homeassistant.setup import async_setup_component, async_process_deps_reqs from homeassistant.util.decorator import Registry @@ -161,9 +160,15 @@ PATH_CONFIG = '.config_entries.json' SAVE_DELAY = 1 +# The config entry has been set up successfully ENTRY_STATE_LOADED = 'loaded' +# There was an error while trying to set up this config entry ENTRY_STATE_SETUP_ERROR = 'setup_error' +# The config entry was not ready to be set up yet, but might be later +ENTRY_STATE_SETUP_RETRY = 'setup_retry' +# The config entry has not been loaded ENTRY_STATE_NOT_LOADED = 'not_loaded' +# An error occurred when trying to unload the entry ENTRY_STATE_FAILED_UNLOAD = 'failed_unload' DISCOVERY_NOTIFICATION_ID = 'config_entry_discovery' @@ -186,7 +191,8 @@ class ConfigEntry: """Hold a configuration entry.""" __slots__ = ('entry_id', 'version', 'domain', 'title', 'data', 'source', - 'connection_class', 'state') + 'connection_class', 'state', '_setup_lock', + '_async_cancel_retry_setup') def __init__(self, version: str, domain: str, title: str, data: dict, source: str, connection_class: str, @@ -217,8 +223,11 @@ class ConfigEntry: # State of the entry (LOADED, NOT_LOADED) self.state = state + # Function to cancel a scheduled retry + self._async_cancel_retry_setup = None + async def async_setup( - self, hass: HomeAssistant, *, component=None) -> None: + self, hass: HomeAssistant, *, component=None, tries=0) -> None: """Set up an entry.""" if component is None: component = getattr(hass.components, self.domain) @@ -230,6 +239,22 @@ class ConfigEntry: _LOGGER.error('%s.async_config_entry did not return boolean', component.DOMAIN) result = False + except ConfigEntryNotReady: + self.state = ENTRY_STATE_SETUP_RETRY + wait_time = 2**min(tries, 4) * 5 + tries += 1 + _LOGGER.warning( + 'Config entry for %s not ready yet. Retrying in %d seconds.', + self.domain, wait_time) + + async def setup_again(now): + """Run setup again.""" + self._async_cancel_retry_setup = None + await self.async_setup(hass, component=component, tries=tries) + + self._async_cancel_retry_setup = \ + hass.helpers.event.async_call_later(wait_time, setup_again) + return except Exception: # pylint: disable=broad-except _LOGGER.exception('Error setting up entry %s for %s', self.title, component.DOMAIN) @@ -252,6 +277,15 @@ class ConfigEntry: if component is None: component = getattr(hass.components, self.domain) + if component.DOMAIN == self.domain: + if self._async_cancel_retry_setup is not None: + self._async_cancel_retry_setup() + self.state = ENTRY_STATE_NOT_LOADED + return True + + if self.state != ENTRY_STATE_LOADED: + return True + supports_unload = hasattr(component, 'async_unload_entry') if not supports_unload: @@ -260,16 +294,18 @@ class ConfigEntry: try: result = await component.async_unload_entry(hass, self) - if not isinstance(result, bool): - _LOGGER.error('%s.async_unload_entry did not return boolean', - component.DOMAIN) - result = False + assert isinstance(result, bool) + + # Only adjust state if we unloaded the component + if result and component.DOMAIN == self.domain: + self.state = ENTRY_STATE_NOT_LOADED return result except Exception: # pylint: disable=broad-except _LOGGER.exception('Error unloading entry %s for %s', self.title, component.DOMAIN) - self.state = ENTRY_STATE_FAILED_UNLOAD + if component.DOMAIN == self.domain: + self.state = ENTRY_STATE_FAILED_UNLOAD return False def as_dict(self): @@ -342,10 +378,7 @@ class ConfigEntries: entry = self._entries.pop(found) self._async_schedule_save() - if entry.state == ENTRY_STATE_LOADED: - unloaded = await entry.async_unload(self.hass) - else: - unloaded = True + unloaded = await entry.async_unload(self.hass) device_registry = await \ self.hass.helpers.device_registry.async_get_registry() diff --git a/homeassistant/exceptions.py b/homeassistant/exceptions.py index 73bd2377950..11aa1848529 100644 --- a/homeassistant/exceptions.py +++ b/homeassistant/exceptions.py @@ -35,6 +35,12 @@ class PlatformNotReady(HomeAssistantError): pass +class ConfigEntryNotReady(HomeAssistantError): + """Error to indicate that config entry is not ready.""" + + pass + + class InvalidStateError(HomeAssistantError): """When an invalid state is encountered.""" diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 8f12407c6b7..340118502b1 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -6,6 +6,7 @@ from unittest.mock import MagicMock, patch import pytest from homeassistant import config_entries, loader, data_entry_flow +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.setup import async_setup_component from homeassistant.util import dt @@ -126,7 +127,7 @@ def test_remove_entry_if_not_loaded(hass, manager): assert [item.entry_id for item in manager.async_entries()] == \ ['test1', 'test3'] - assert len(mock_unload_entry.mock_calls) == 0 + assert len(mock_unload_entry.mock_calls) == 1 @asyncio.coroutine @@ -367,3 +368,49 @@ async def test_updating_entry_data(manager): assert entry.data == { 'second': True } + + +async def test_setup_raise_not_ready(hass, caplog): + """Test a setup raising not ready.""" + entry = MockConfigEntry(domain='test') + + mock_setup_entry = MagicMock(side_effect=ConfigEntryNotReady) + loader.set_component( + hass, 'test', MockModule('test', async_setup_entry=mock_setup_entry)) + + with patch('homeassistant.helpers.event.async_call_later') as mock_call: + await entry.async_setup(hass) + + assert len(mock_call.mock_calls) == 1 + assert 'Config entry for test not ready yet' in caplog.text + p_hass, p_wait_time, p_setup = mock_call.mock_calls[0][1] + + assert p_hass is hass + assert p_wait_time == 5 + assert entry.state == config_entries.ENTRY_STATE_SETUP_RETRY + + mock_setup_entry.side_effect = None + mock_setup_entry.return_value = mock_coro(True) + + await p_setup(None) + assert entry.state == config_entries.ENTRY_STATE_LOADED + + +async def test_setup_retrying_during_unload(hass): + """Test if we unload an entry that is in retry mode.""" + entry = MockConfigEntry(domain='test') + + mock_setup_entry = MagicMock(side_effect=ConfigEntryNotReady) + loader.set_component( + hass, 'test', MockModule('test', async_setup_entry=mock_setup_entry)) + + with patch('homeassistant.helpers.event.async_call_later') as mock_call: + await entry.async_setup(hass) + + assert entry.state == config_entries.ENTRY_STATE_SETUP_RETRY + assert len(mock_call.return_value.mock_calls) == 0 + + await entry.async_unload(hass) + + assert entry.state == config_entries.ENTRY_STATE_NOT_LOADED + assert len(mock_call.return_value.mock_calls) == 1 From b92b24e8a47d4bc244fa1da3dc9e965161427ee4 Mon Sep 17 00:00:00 2001 From: Georgi Kirichkov Date: Thu, 4 Oct 2018 16:54:51 +0300 Subject: [PATCH 193/247] Webhook component - pass headers to webhook handler (#17091) * Pass headers to webhook handler * Refactors webhook component to post Request object instead of data * Update webhook tests * Cleanup webhook test and fix a bug in ifttt * Address code review comments --- homeassistant/components/ifttt/__init__.py | 9 ++++++++- homeassistant/components/webhook.py | 11 +---------- tests/components/test_webhook.py | 8 +++----- 3 files changed, 12 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/ifttt/__init__.py b/homeassistant/components/ifttt/__init__.py index 534217a7ba2..60748d6ff13 100644 --- a/homeassistant/components/ifttt/__init__.py +++ b/homeassistant/components/ifttt/__init__.py @@ -5,6 +5,7 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/ifttt/ """ from ipaddress import ip_address +import json import logging from urllib.parse import urlparse @@ -74,8 +75,14 @@ async def async_setup(hass, config): return True -async def handle_webhook(hass, webhook_id, data): +async def handle_webhook(hass, webhook_id, request): """Handle webhook callback.""" + body = await request.text() + try: + data = json.loads(body) if body else {} + except ValueError: + return None + if isinstance(data, dict): data['webhook_id'] = webhook_id hass.bus.async_fire(EVENT_RECEIVED, data) diff --git a/homeassistant/components/webhook.py b/homeassistant/components/webhook.py index 0e44ffbab25..2a4c3f973f2 100644 --- a/homeassistant/components/webhook.py +++ b/homeassistant/components/webhook.py @@ -3,7 +3,6 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/webhook/ """ -import json import logging from aiohttp.web import Response @@ -76,16 +75,8 @@ class WebhookView(HomeAssistantView): 'Received message for unregistered webhook %s', webhook_id) return Response(status=200) - body = await request.text() try: - data = json.loads(body) if body else {} - except ValueError: - _LOGGER.warning( - 'Received webhook %s with invalid JSON', webhook_id) - return Response(status=200) - - try: - response = await handler(hass, webhook_id, data) + response = await handler(hass, webhook_id, request) if response is None: response = Response(status=200) return response diff --git a/tests/components/test_webhook.py b/tests/components/test_webhook.py index c87687292a8..9434c3d98d5 100644 --- a/tests/components/test_webhook.py +++ b/tests/components/test_webhook.py @@ -63,7 +63,7 @@ async def test_posting_webhook_json(hass, mock_client): async def handle(*args): """Handle webhook.""" - hooks.append(args) + hooks.append((args[0], args[1], await args[2].text())) hass.components.webhook.async_register(webhook_id, handle) @@ -74,9 +74,7 @@ async def test_posting_webhook_json(hass, mock_client): assert len(hooks) == 1 assert hooks[0][0] is hass assert hooks[0][1] == webhook_id - assert hooks[0][2] == { - 'data': True - } + assert hooks[0][2] == '{"data": true}' async def test_posting_webhook_no_data(hass, mock_client): @@ -95,4 +93,4 @@ async def test_posting_webhook_no_data(hass, mock_client): assert len(hooks) == 1 assert hooks[0][0] is hass assert hooks[0][1] == webhook_id - assert hooks[0][2] == {} + assert await hooks[0][2].text() == '' From 02bf07d9dfd8beacd8cec15a2ec9b9e037d1af82 Mon Sep 17 00:00:00 2001 From: Ana Paula Gomes Date: Thu, 4 Oct 2018 16:02:14 +0200 Subject: [PATCH 194/247] Add timeout and fix oscillations on Samsung TV component (#17102) * Add timeout and fix oscillations * Adjust code to py3.5.3 * Clean code --- .../components/media_player/samsungtv.py | 10 +++++++--- tests/components/media_player/test_samsungtv.py | 17 +++++++++++++++++ 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/media_player/samsungtv.py b/homeassistant/components/media_player/samsungtv.py index c0a5d617f19..3573b28b4e3 100644 --- a/homeassistant/components/media_player/samsungtv.py +++ b/homeassistant/components/media_player/samsungtv.py @@ -125,10 +125,14 @@ class SamsungTVDevice(MediaPlayerDevice): def update(self): """Update state of device.""" if sys.platform == 'win32': - _ping_cmd = ['ping', '-n 1', '-w', '1000', self._config['host']] + timeout_arg = '-w {}000'.format(self._config['timeout']) + _ping_cmd = [ + 'ping', '-n 3', timeout_arg, self._config['host']] else: - _ping_cmd = ['ping', '-n', '-q', '-c1', '-W1', - self._config['host']] + timeout_arg = '-W{}'.format(self._config['timeout']) + _ping_cmd = [ + 'ping', '-n', '-q', + '-c3', timeout_arg, self._config['host']] ping = subprocess.Popen( _ping_cmd, diff --git a/tests/components/media_player/test_samsungtv.py b/tests/components/media_player/test_samsungtv.py index 2fedfb6a65e..5551a86df05 100644 --- a/tests/components/media_player/test_samsungtv.py +++ b/tests/components/media_player/test_samsungtv.py @@ -128,6 +128,23 @@ class TestSamsungTv(unittest.TestCase): self.device.update() self.assertEqual(STATE_OFF, self.device._state) + @mock.patch( + 'homeassistant.components.media_player.samsungtv.subprocess.Popen' + ) + def test_timeout(self, mocked_popen): + """Test timeout use.""" + ping = mock.Mock() + mocked_popen.return_value = ping + ping.returncode = 0 + self.device.update() + expected_timeout = self.device._config['timeout'] + timeout_arg = '-W{}'.format(expected_timeout) + ping_command = [ + 'ping', '-n', '-q', '-c3', timeout_arg, 'fake'] + expected_call = call(ping_command, stderr=-3, stdout=-1) + self.assertEqual(mocked_popen.call_args, expected_call) + self.assertEqual(STATE_ON, self.device._state) + def test_send_key(self): """Test for send key.""" self.device.send_key('KEY_POWER') From 769dda735d963409a1214e151bb60e7dfdb4cb91 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 4 Oct 2018 16:04:44 +0200 Subject: [PATCH 195/247] Remove discovery (#17070) --- homeassistant/components/hue/__init__.py | 36 +++------------ tests/components/hue/test_init.py | 56 ------------------------ 2 files changed, 5 insertions(+), 87 deletions(-) diff --git a/homeassistant/components/hue/__init__.py b/homeassistant/components/hue/__init__.py index 8431e5dbe3d..9c28d08054b 100644 --- a/homeassistant/components/hue/__init__.py +++ b/homeassistant/components/hue/__init__.py @@ -12,9 +12,9 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_FILENAME, CONF_HOST from homeassistant.helpers import ( - aiohttp_client, config_validation as cv, device_registry as dr) + config_validation as cv, device_registry as dr) -from .const import DOMAIN, API_NUPNP +from .const import DOMAIN from .bridge import HueBridge # Loading the config flow file will register the flow from .config_flow import configured_hosts @@ -62,37 +62,11 @@ async def async_setup(hass, config): configured = configured_hosts(hass) # User has configured bridges - if CONF_BRIDGES in conf: - bridges = conf[CONF_BRIDGES] - - # Component is part of config but no bridges specified, discover. - elif DOMAIN in config: - # discover from nupnp - websession = aiohttp_client.async_get_clientsession(hass) - - async with websession.get(API_NUPNP) as req: - hosts = await req.json() - - bridges = [] - for entry in hosts: - # Filter out already configured hosts - if entry['internalipaddress'] in configured: - continue - - # Run through config schema to populate defaults - bridges.append(BRIDGE_CONFIG_SCHEMA({ - CONF_HOST: entry['internalipaddress'], - # Careful with using entry['id'] for other reasons. The - # value is in lowercase but is returned uppercase from hub. - CONF_FILENAME: '.hue_{}.conf'.format(entry['id']), - })) - else: - # Component not specified in config, we're loaded via discovery - bridges = [] - - if not bridges: + if CONF_BRIDGES not in conf: return True + bridges = conf[CONF_BRIDGES] + for bridge_conf in bridges: host = bridge_conf[CONF_HOST] diff --git a/tests/components/hue/test_init.py b/tests/components/hue/test_init.py index 5da6d5b709a..1fcc092dd30 100644 --- a/tests/components/hue/test_init.py +++ b/tests/components/hue/test_init.py @@ -20,62 +20,6 @@ async def test_setup_with_no_config(hass): assert hass.data[hue.DOMAIN] == {} -async def test_setup_with_discovery_no_known_auth(hass, aioclient_mock): - """Test discovering a bridge and not having known auth.""" - aioclient_mock.get(hue.API_NUPNP, json=[ - { - 'internalipaddress': '0.0.0.0', - 'id': 'abcd1234' - } - ]) - - with patch.object(hass, 'config_entries') as mock_config_entries, \ - patch.object(hue, 'configured_hosts', return_value=[]): - mock_config_entries.flow.async_init.return_value = mock_coro() - assert await async_setup_component(hass, hue.DOMAIN, { - hue.DOMAIN: {} - }) is True - - # Flow started for discovered bridge - assert len(mock_config_entries.flow.mock_calls) == 1 - assert mock_config_entries.flow.mock_calls[0][2]['data'] == { - 'host': '0.0.0.0', - 'path': '.hue_abcd1234.conf', - } - - # Config stored for domain. - assert hass.data[hue.DOMAIN] == { - '0.0.0.0': { - hue.CONF_HOST: '0.0.0.0', - hue.CONF_FILENAME: '.hue_abcd1234.conf', - hue.CONF_ALLOW_HUE_GROUPS: hue.DEFAULT_ALLOW_HUE_GROUPS, - hue.CONF_ALLOW_UNREACHABLE: hue.DEFAULT_ALLOW_UNREACHABLE, - } - } - - -async def test_setup_with_discovery_known_auth(hass, aioclient_mock): - """Test we don't do anything if we discover already configured hub.""" - aioclient_mock.get(hue.API_NUPNP, json=[ - { - 'internalipaddress': '0.0.0.0', - 'id': 'abcd1234' - } - ]) - - with patch.object(hass, 'config_entries') as mock_config_entries, \ - patch.object(hue, 'configured_hosts', return_value=['0.0.0.0']): - assert await async_setup_component(hass, hue.DOMAIN, { - hue.DOMAIN: {} - }) is True - - # Flow started for discovered bridge - assert len(mock_config_entries.flow.mock_calls) == 0 - - # Config stored for domain. - assert hass.data[hue.DOMAIN] == {} - - async def test_setup_defined_hosts_known_auth(hass): """Test we don't initiate a config entry if config bridge is known.""" with patch.object(hass, 'config_entries') as mock_config_entries, \ From 52ff23279793ee6f6dc251d64583ff2fd04e8049 Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Thu, 4 Oct 2018 20:37:04 +0200 Subject: [PATCH 196/247] Bugfix invalid entity_config parameter HomeKit (#17143) --- homeassistant/components/homekit/util.py | 3 +++ tests/components/homekit/test_util.py | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/homekit/util.py b/homeassistant/components/homekit/util.py index 9d60530edd7..33a4bec321f 100644 --- a/homeassistant/components/homekit/util.py +++ b/homeassistant/components/homekit/util.py @@ -44,6 +44,9 @@ SWITCH_TYPE_SCHEMA = BASIC_INFO_SCHEMA.extend({ def validate_entity_config(values): """Validate config entry for CONF_ENTITY.""" + if not isinstance(values, dict): + raise vol.Invalid('expected a dictionary') + entities = {} for entity_id, config in values.items(): entity = cv.entity_id(entity_id) diff --git a/tests/components/homekit/test_util.py b/tests/components/homekit/test_util.py index 9be92b817be..6ae16ee8f33 100644 --- a/tests/components/homekit/test_util.py +++ b/tests/components/homekit/test_util.py @@ -23,7 +23,8 @@ from tests.common import async_mock_service def test_validate_entity_config(): """Test validate entities.""" - configs = [{'invalid_entity_id': {}}, {'demo.test': 1}, + configs = [None, [], 'string', 12345, + {'invalid_entity_id': {}}, {'demo.test': 1}, {'demo.test': 'test'}, {'demo.test': [1, 2]}, {'demo.test': None}, {'demo.test': {CONF_NAME: None}}, {'media_player.test': {CONF_FEATURE_LIST: [ From b55c7a5ba243730b27047b958f6b6c9fd64e3ac8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Per=20Sandstr=C3=B6m?= Date: Thu, 4 Oct 2018 21:41:02 +0200 Subject: [PATCH 197/247] verisure configurable polling (#17144) --- homeassistant/components/verisure.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/verisure.py b/homeassistant/components/verisure.py index 1f26ab639d6..3876ff41c37 100644 --- a/homeassistant/components/verisure.py +++ b/homeassistant/components/verisure.py @@ -32,6 +32,7 @@ CONF_MOUSE = 'mouse' CONF_SMARTPLUGS = 'smartplugs' CONF_THERMOMETERS = 'thermometers' CONF_SMARTCAM = 'smartcam' +CONF_POLLING_RATE = 'polling_rate' DOMAIN = 'verisure' @@ -53,6 +54,8 @@ CONFIG_SCHEMA = vol.Schema({ vol.Optional(CONF_SMARTPLUGS, default=True): cv.boolean, vol.Optional(CONF_THERMOMETERS, default=True): cv.boolean, vol.Optional(CONF_SMARTCAM, default=True): cv.boolean, + vol.Optional(CONF_POLLING_RATE, default=1): vol.All( + vol.Coerce(int), vol.Range(min=1)), }), }, extra=vol.ALLOW_EXTRA) @@ -66,6 +69,8 @@ def setup(hass, config): import verisure global HUB HUB = VerisureHub(config[DOMAIN], verisure) + HUB.update_overview = Throttle(timedelta( + minutes=config[DOMAIN][CONF_POLLING_RATE]))(HUB.update_overview) if not HUB.login(): return False hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, @@ -140,7 +145,6 @@ class VerisureHub: return False return True - @Throttle(timedelta(seconds=60)) def update_overview(self): """Update the overview.""" try: From ce1e8f8ace3c2961542049cc529b77a7bfde173f Mon Sep 17 00:00:00 2001 From: Florian Klien Date: Thu, 4 Oct 2018 23:34:04 +0200 Subject: [PATCH 198/247] YesssSMS handling more errors, upgrade to version 0.2.3 (#17052) * YesssSMS handling more errors, upgrade to version 0.2.1 - handling missing internet connection nicely - disabling login with non-working credentials (website locked account for 1 hour) - upgrade to new upstream version of YesssSMS * notify.yessssms tests * test requirements * flake8 fix * fixing tests, new upstream version 0.2.3 fixing tests based on requested changes, coverage * removing unmotivated print * passing exception to ConnectionError and SMSSendingError logger --- .coveragerc | 1 - homeassistant/components/notify/yessssms.py | 39 +++- requirements_all.txt | 2 +- requirements_test_all.txt | 3 + script/gen_requirements_all.py | 3 +- tests/components/notify/test_yessssms.py | 208 ++++++++++++++++++++ 6 files changed, 246 insertions(+), 10 deletions(-) create mode 100644 tests/components/notify/test_yessssms.py diff --git a/.coveragerc b/.coveragerc index 378dba5ef2a..d5296455981 100644 --- a/.coveragerc +++ b/.coveragerc @@ -630,7 +630,6 @@ omit = homeassistant/components/notify/telstra.py homeassistant/components/notify/twitter.py homeassistant/components/notify/xmpp.py - homeassistant/components/notify/yessssms.py homeassistant/components/nuimo_controller.py homeassistant/components/prometheus.py homeassistant/components/rainbird.py diff --git a/homeassistant/components/notify/yessssms.py b/homeassistant/components/notify/yessssms.py index 37a6a90a62e..e16e384ca25 100644 --- a/homeassistant/components/notify/yessssms.py +++ b/homeassistant/components/notify/yessssms.py @@ -13,7 +13,8 @@ from homeassistant.components.notify import ( from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_RECIPIENT import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['YesssSMS==0.1.1b3'] + +REQUIREMENTS = ['YesssSMS==0.2.3'] _LOGGER = logging.getLogger(__name__) @@ -38,14 +39,38 @@ class YesssSMSNotificationService(BaseNotificationService): from YesssSMS import YesssSMS self.yesss = YesssSMS(username, password) self._recipient = recipient + _LOGGER.debug( + "initialized; library version: %s", self.yesss.version()) def send_message(self, message="", **kwargs): """Send a SMS message via Yesss.at's website.""" + if self.yesss.account_is_suspended(): + # only retry to login after HASS was restarted with (hopefully) + # new login data. + _LOGGER.error( + "Account is suspended, cannot send SMS. " + "Check your login data and restart Home Assistant") + return try: self.yesss.send(self._recipient, message) - except ValueError as ex: - if str(ex).startswith("YesssSMS:"): - _LOGGER.error(str(ex)) - except RuntimeError as ex: - if str(ex).startswith("YesssSMS:"): - _LOGGER.error(str(ex)) + except self.yesss.NoRecipientError as ex: + _LOGGER.error( + "You need to provide a recipient for SMS notification: %s", + ex) + except self.yesss.EmptyMessageError as ex: + _LOGGER.error( + "Cannot send empty SMS message: %s", ex) + except self.yesss.SMSSendingError as ex: + _LOGGER.error(str(ex), exc_info=ex) + except ConnectionError as ex: + _LOGGER.error( + "YesssSMS: unable to connect to yesss.at server.", + exc_info=ex) + except self.yesss.AccountSuspendedError as ex: + _LOGGER.error( + "Wrong login credentials!! Verify correct credentials and " + "restart Home Assistant: %s", ex) + except self.yesss.LoginError as ex: + _LOGGER.error("Wrong login credentials: %s", ex) + else: + _LOGGER.info("SMS sent") diff --git a/requirements_all.txt b/requirements_all.txt index 82a0571b001..fca9e771b52 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -76,7 +76,7 @@ TwitterAPI==2.5.4 WazeRouteCalculator==0.6 # homeassistant.components.notify.yessssms -YesssSMS==0.1.1b3 +YesssSMS==0.2.3 # homeassistant.components.abode abodepy==0.13.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 44d360f23ad..10b8307d337 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -24,6 +24,9 @@ HAP-python==2.2.2 # homeassistant.components.sensor.rmvtransport PyRMVtransport==0.1 +# homeassistant.components.notify.yessssms +YesssSMS==0.2.3 + # homeassistant.components.device_tracker.automatic aioautomatic==0.6.5 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index c6e36c5c83e..9c0323bf5ca 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -104,7 +104,8 @@ TEST_REQUIREMENTS = ( 'yahoo-finance', 'pythonwhois', 'wakeonlan', - 'vultr' + 'vultr', + 'YesssSMS', ) IGNORE_PACKAGES = ( diff --git a/tests/components/notify/test_yessssms.py b/tests/components/notify/test_yessssms.py new file mode 100644 index 00000000000..42dd7be1aca --- /dev/null +++ b/tests/components/notify/test_yessssms.py @@ -0,0 +1,208 @@ +"""The tests for the notify yessssms platform.""" +import unittest +import requests_mock +from homeassistant.components.notify import yessssms + + +class TestNotifyYesssSMS(unittest.TestCase): + """Test the yessssms notify.""" + + def setUp(self): # pylint: disable=invalid-name + """Set up things to be run when tests are started.""" + login = "06641234567" + passwd = "testpasswd" + recipient = "06501234567" + self.yessssms = yessssms.YesssSMSNotificationService( + login, passwd, recipient) + + @requests_mock.Mocker() + def test_login_error(self, mock): + """Test login that fails.""" + mock.register_uri( + requests_mock.POST, + # pylint: disable=protected-access + self.yessssms.yesss._login_url, + status_code=200, + text="BlaBlaBlaLogin nicht erfolgreichBlaBla" + ) + + message = "Testing YesssSMS platform :)" + + with self.assertLogs("homeassistant.components.notify", + level='ERROR'): + self.yessssms.send_message(message) + self.assertTrue(mock.called) + self.assertEqual(mock.call_count, 1) + + def test_empty_message_error(self): + """Test for an empty SMS message error.""" + message = "" + with self.assertLogs("homeassistant.components.notify", + level='ERROR'): + self.yessssms.send_message(message) + + @requests_mock.Mocker() + def test_error_account_suspended(self, mock): + """Test login that fails after multiple attempts.""" + mock.register_uri( + 'POST', + # pylint: disable=protected-access + self.yessssms.yesss._login_url, + status_code=200, + text="BlaBlaBlaLogin nicht erfolgreichBlaBla" + ) + + message = "Testing YesssSMS platform :)" + + with self.assertLogs("homeassistant.components.notify", + level='ERROR'): + self.yessssms.send_message(message) + self.assertTrue(mock.called) + self.assertEqual(mock.call_count, 1) + + mock.register_uri( + 'POST', + # pylint: disable=protected-access + self.yessssms.yesss._login_url, + status_code=200, + text="Wegen 3 ungültigen Login-Versuchen ist Ihr Account für " + "eine Stunde gesperrt." + ) + + message = "Testing YesssSMS platform :)" + + with self.assertLogs("homeassistant.components.notify", + level='ERROR'): + self.yessssms.send_message(message) + self.assertTrue(mock.called) + self.assertEqual(mock.call_count, 2) + + def test_error_account_suspended_2(self): + """Test login that fails after multiple attempts.""" + message = "Testing YesssSMS platform :)" + # pylint: disable=protected-access + self.yessssms.yesss._suspended = True + + with self.assertLogs("homeassistant.components.notify", + level='ERROR') as context: + self.yessssms.send_message(message) + self.assertIn("Account is suspended, cannot send SMS.", + context.output[0]) + + @requests_mock.Mocker() + def test_send_message(self, mock): + """Test send message.""" + message = "Testing YesssSMS platform :)" + mock.register_uri( + 'POST', + # pylint: disable=protected-access + self.yessssms.yesss._login_url, + status_code=302, + # pylint: disable=protected-access + headers={'location': self.yessssms.yesss._kontomanager} + ) + # pylint: disable=protected-access + login = self.yessssms.yesss._logindata['login_rufnummer'] + mock.register_uri( + 'GET', + # pylint: disable=protected-access + self.yessssms.yesss._kontomanager, + status_code=200, + text="test..." + login + "" + ) + mock.register_uri( + 'POST', + # pylint: disable=protected-access + self.yessssms.yesss._websms_url, + status_code=200, + text="

Ihre SMS wurde erfolgreich verschickt!

" + ) + mock.register_uri( + 'GET', + # pylint: disable=protected-access + self.yessssms.yesss._logout_url, + status_code=200, + ) + + with self.assertLogs("homeassistant.components.notify", + level='INFO') as context: + self.yessssms.send_message(message) + self.assertIn("SMS sent", context.output[0]) + self.assertTrue(mock.called) + self.assertEqual(mock.call_count, 4) + self.assertIn(mock.last_request.scheme + "://" + + mock.last_request.hostname + + mock.last_request.path + "?" + + mock.last_request.query, + # pylint: disable=protected-access + self.yessssms.yesss._logout_url) + + def test_no_recipient_error(self): + """Test for missing/empty recipient.""" + message = "Testing YesssSMS platform :)" + # pylint: disable=protected-access + self.yessssms._recipient = "" + + with self.assertLogs("homeassistant.components.notify", + level='ERROR') as context: + self.yessssms.send_message(message) + + self.assertIn("You need to provide a recipient for SMS notification", + context.output[0]) + + @requests_mock.Mocker() + def test_sms_sending_error(self, mock): + """Test sms sending error.""" + mock.register_uri( + 'POST', + # pylint: disable=protected-access + self.yessssms.yesss._login_url, + status_code=302, + # pylint: disable=protected-access + headers={'location': self.yessssms.yesss._kontomanager} + ) + # pylint: disable=protected-access + login = self.yessssms.yesss._logindata['login_rufnummer'] + mock.register_uri( + 'GET', + # pylint: disable=protected-access + self.yessssms.yesss._kontomanager, + status_code=200, + text="test..." + login + "" + ) + mock.register_uri( + 'POST', + # pylint: disable=protected-access + self.yessssms.yesss._websms_url, + status_code=500 + ) + + message = "Testing YesssSMS platform :)" + + with self.assertLogs("homeassistant.components.notify", + level='ERROR') as context: + self.yessssms.send_message(message) + + self.assertTrue(mock.called) + self.assertEqual(mock.call_count, 3) + self.assertIn("YesssSMS: error sending SMS", context.output[0]) + + @requests_mock.Mocker() + def test_connection_error(self, mock): + """Test connection error.""" + mock.register_uri( + 'POST', + # pylint: disable=protected-access + self.yessssms.yesss._login_url, + exc=ConnectionError + ) + + message = "Testing YesssSMS platform :)" + + with self.assertLogs("homeassistant.components.notify", + level='ERROR') as context: + self.yessssms.send_message(message) + + self.assertTrue(mock.called) + self.assertEqual(mock.call_count, 1) + self.assertIn("unable to connect", context.output[0]) From acf684de051c0ab58e73f129281dbbe23ee9d23a Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Thu, 4 Oct 2018 19:57:52 -0600 Subject: [PATCH 199/247] Added OpenUV CODEOWNERS info (#17149) --- CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CODEOWNERS b/CODEOWNERS index 3bf22d52d67..00ca296c1d2 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -111,7 +111,7 @@ homeassistant/components/konnected.py @heythisisnate homeassistant/components/*/konnected.py @heythisisnate homeassistant/components/matrix.py @tinloaf homeassistant/components/*/matrix.py @tinloaf -homeassistant/components/openuv.py @bachya +homeassistant/components/openuv/* @bachya homeassistant/components/*/openuv.py @bachya homeassistant/components/qwikswitch.py @kellerza homeassistant/components/*/qwikswitch.py @kellerza From aec320dc193b796c620c6e27b49b4719705b354c Mon Sep 17 00:00:00 2001 From: Gerard Date: Fri, 5 Oct 2018 07:48:40 +0200 Subject: [PATCH 200/247] Fix a typo (#17147) --- homeassistant/components/switch/volvooncall.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/switch/volvooncall.py b/homeassistant/components/switch/volvooncall.py index 96091a725a1..42c753725ab 100644 --- a/homeassistant/components/switch/volvooncall.py +++ b/homeassistant/components/switch/volvooncall.py @@ -15,7 +15,7 @@ _LOGGER = logging.getLogger(__name__) def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up Tellstick switches.""" + """Set up a Volvo switch.""" if discovery_info is None: return add_entities([VolvoSwitch(hass, *discovery_info)]) From 4218efddcda217dd8c28f7ea463a52f96bd19196 Mon Sep 17 00:00:00 2001 From: Lewis Juggins Date: Fri, 5 Oct 2018 08:59:34 +0100 Subject: [PATCH 201/247] Cleanly stop tradfri on shutdown (#17114) * Tradfri shutdown fix * Bump version * Bump version * Fix * Derp * Remove unnecessary shutdown event --- homeassistant/components/tradfri/__init__.py | 10 +++++++++- homeassistant/components/tradfri/config_flow.py | 3 +++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 14 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tradfri/__init__.py b/homeassistant/components/tradfri/__init__.py index 51195d0a168..e9ee8d0898b 100644 --- a/homeassistant/components/tradfri/__init__.py +++ b/homeassistant/components/tradfri/__init__.py @@ -9,6 +9,7 @@ import logging import voluptuous as vol from homeassistant import config_entries +from homeassistant.const import EVENT_HOMEASSISTANT_STOP import homeassistant.helpers.config_validation as cv from homeassistant.util.json import load_json @@ -17,7 +18,7 @@ from .const import ( from . import config_flow # noqa pylint_disable=unused-import -REQUIREMENTS = ['pytradfri[async]==5.6.0'] +REQUIREMENTS = ['pytradfri[async]==6.0.1'] DOMAIN = 'tradfri' CONFIG_FILE = '.tradfri_psk.conf' @@ -87,6 +88,13 @@ async def async_setup_entry(hass, entry): psk=entry.data[CONF_KEY], loop=hass.loop ) + + async def on_hass_stop(event): + """Close connection when hass stops.""" + await factory.shutdown() + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, on_hass_stop) + api = factory.request gateway = Gateway() diff --git a/homeassistant/components/tradfri/config_flow.py b/homeassistant/components/tradfri/config_flow.py index f3c52f42a9a..a3452e50c4d 100644 --- a/homeassistant/components/tradfri/config_flow.py +++ b/homeassistant/components/tradfri/config_flow.py @@ -166,9 +166,12 @@ async def get_gateway_info(hass, host, identity, key): psk=key, loop=hass.loop ) + api = factory.request gateway = Gateway() gateway_info_result = await api(gateway.get_gateway_info()) + + await factory.shutdown() except (OSError, RequestError): # We're also catching OSError as PyTradfri doesn't catch that one yet # Upstream PR: https://github.com/ggravlingen/pytradfri/pull/189 diff --git a/requirements_all.txt b/requirements_all.txt index fca9e771b52..c634c194c7e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1217,7 +1217,7 @@ pytouchline==0.7 pytrackr==0.0.5 # homeassistant.components.tradfri -pytradfri[async]==5.6.0 +pytradfri[async]==6.0.1 # homeassistant.components.sensor.trafikverket_weatherstation pytrafikverket==0.1.5.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 10b8307d337..4f27664e7df 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -188,7 +188,7 @@ python-nest==4.0.3 pythonwhois==2.4.3 # homeassistant.components.tradfri -pytradfri[async]==5.6.0 +pytradfri[async]==6.0.1 # homeassistant.components.device_tracker.unifi pyunifi==2.13 From a8f5e8699a8d223f5a514efb0c694fd02a73349e Mon Sep 17 00:00:00 2001 From: Rohan Kapoor Date: Fri, 5 Oct 2018 01:30:08 -0700 Subject: [PATCH 202/247] Fix zoneminder zms_url construction (#17150) --- homeassistant/components/zoneminder.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zoneminder.py b/homeassistant/components/zoneminder.py index b7f43177200..3f6d8ba7fcf 100644 --- a/homeassistant/components/zoneminder.py +++ b/homeassistant/components/zoneminder.py @@ -15,7 +15,7 @@ import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['zm-py==0.0.4'] +REQUIREMENTS = ['zm-py==0.0.5'] CONF_PATH_ZMS = 'path_zms' diff --git a/requirements_all.txt b/requirements_all.txt index c634c194c7e..25f0eeac147 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1574,4 +1574,4 @@ zigpy-xbee==0.1.1 zigpy==0.2.0 # homeassistant.components.zoneminder -zm-py==0.0.4 +zm-py==0.0.5 From 2e62afabdc232602289bdbe858ad95145ed29511 Mon Sep 17 00:00:00 2001 From: Julius Mittenzwei Date: Fri, 5 Oct 2018 12:32:26 +0200 Subject: [PATCH 203/247] Added warning to HomeKit component (#16807) * added warning if more then 100 devices are added to HomeKit --- homeassistant/components/homekit/__init__.py | 10 ++++++-- tests/components/homekit/test_homekit.py | 24 ++++++++++++++++---- 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 8c12243ee8f..bd79a0a7ca7 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -28,10 +28,12 @@ from .const import ( from .util import ( show_setup_message, validate_entity_config, validate_media_player_features) -TYPES = Registry() +REQUIREMENTS = ['HAP-python==2.2.2'] + _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['HAP-python==2.2.2'] +MAX_DEVICES = 100 +TYPES = Registry() # #### Driver Status #### STATUS_READY = 0 @@ -239,6 +241,10 @@ class HomeKit(): if not self.driver.state.paired: show_setup_message(self.hass, self.driver.state.pincode) + if len(self.bridge.accessories) > MAX_DEVICES: + _LOGGER.warning('You have exceeded the device limit, which might ' + 'cause issues. Consider using the filter option.') + _LOGGER.debug('Driver start') self.hass.add_job(self.driver.start) self.status = STATUS_RUNNING diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py index f8afb4a49ab..a831a7e9e5d 100644 --- a/tests/components/homekit/test_homekit.py +++ b/tests/components/homekit/test_homekit.py @@ -5,8 +5,8 @@ import pytest from homeassistant import setup from homeassistant.components.homekit import ( - generate_aid, HomeKit, STATUS_READY, STATUS_RUNNING, - STATUS_STOPPED, STATUS_WAIT) + generate_aid, HomeKit, MAX_DEVICES, STATUS_READY, + STATUS_RUNNING, STATUS_STOPPED, STATUS_WAIT) from homeassistant.components.homekit.accessories import HomeBridge from homeassistant.components.homekit.const import ( CONF_AUTO_START, BRIDGE_NAME, DEFAULT_PORT, DOMAIN, HOMEKIT_FILE, @@ -173,7 +173,8 @@ async def test_homekit_start(hass, hk_driver, debounce_patcher): """Test HomeKit start method.""" pin = b'123-45-678' homekit = HomeKit(hass, None, None, None, {}, {'cover.demo': {}}) - homekit.bridge = 'bridge' + homekit.bridge = Mock() + homekit.bridge.accessories = [] homekit.driver = hk_driver hass.states.async_set('light.demo', 'on') @@ -190,7 +191,7 @@ async def test_homekit_start(hass, hk_driver, debounce_patcher): mock_add_acc.assert_called_with(state) mock_setup_msg.assert_called_with(hass, pin) - hk_driver_add_acc.assert_called_with('bridge') + hk_driver_add_acc.assert_called_with(homekit.bridge) assert hk_driver_start.called assert homekit.status == STATUS_RUNNING @@ -217,3 +218,18 @@ async def test_homekit_stop(hass): homekit.status = STATUS_RUNNING await hass.async_add_job(homekit.stop) assert homekit.driver.stop.called is True + + +async def test_homekit_too_many_accessories(hass, hk_driver): + """Test adding too many accessories to HomeKit.""" + homekit = HomeKit(hass, None, None, None, None, None) + homekit.bridge = Mock() + homekit.bridge.accessories = range(MAX_DEVICES + 1) + homekit.driver = hk_driver + + with patch('pyhap.accessory_driver.AccessoryDriver.start'), \ + patch('pyhap.accessory_driver.AccessoryDriver.add_accessory'), \ + patch('homeassistant.components.homekit._LOGGER.warning') \ + as mock_warn: + await hass.async_add_job(homekit.start) + assert mock_warn.called is True From 37a47b5a59ec3ba5bd05fb3525874c0572a152ec Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Fri, 5 Oct 2018 12:43:50 +0200 Subject: [PATCH 204/247] Add faucet, shower, sprinkler, valve to HomeKit (#17145) --- homeassistant/components/homekit/__init__.py | 12 ++- homeassistant/components/homekit/const.py | 7 ++ .../components/homekit/type_switches.py | 58 ++++++++++++++- homeassistant/components/homekit/util.py | 8 +- .../homekit/test_get_accessories.py | 7 +- .../components/homekit/test_type_switches.py | 74 ++++++++++++++++++- tests/components/homekit/test_util.py | 14 +++- 7 files changed, 168 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index bd79a0a7ca7..5d7733584be 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -24,7 +24,8 @@ from .const import ( BRIDGE_NAME, CONF_AUTO_START, CONF_ENTITY_CONFIG, CONF_FEATURE_LIST, CONF_FILTER, DEFAULT_AUTO_START, DEFAULT_PORT, DEVICE_CLASS_CO, DEVICE_CLASS_CO2, DEVICE_CLASS_PM25, DOMAIN, HOMEKIT_FILE, - SERVICE_HOMEKIT_START, TYPE_OUTLET, TYPE_SWITCH) + SERVICE_HOMEKIT_START, TYPE_FAUCET, TYPE_OUTLET, TYPE_SHOWER, + TYPE_SPRINKLER, TYPE_SWITCH, TYPE_VALVE) from .util import ( show_setup_message, validate_entity_config, validate_media_player_features) @@ -41,8 +42,13 @@ STATUS_RUNNING = 1 STATUS_STOPPED = 2 STATUS_WAIT = 3 -SWITCH_TYPES = {TYPE_OUTLET: 'Outlet', - TYPE_SWITCH: 'Switch'} +SWITCH_TYPES = { + TYPE_FAUCET: 'Valve', + TYPE_OUTLET: 'Outlet', + TYPE_SHOWER: 'Valve', + TYPE_SPRINKLER: 'Valve', + TYPE_SWITCH: 'Switch', + TYPE_VALVE: 'Valve'} CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.All({ diff --git a/homeassistant/components/homekit/const.py b/homeassistant/components/homekit/const.py index df488d4a73a..617dd3f4f22 100644 --- a/homeassistant/components/homekit/const.py +++ b/homeassistant/components/homekit/const.py @@ -32,8 +32,12 @@ BRIDGE_SERIAL_NUMBER = 'homekit.bridge' MANUFACTURER = 'Home Assistant' # #### Switch Types #### +TYPE_FAUCET = 'faucet' TYPE_OUTLET = 'outlet' +TYPE_SHOWER = 'shower' +TYPE_SPRINKLER = 'sprinkler' TYPE_SWITCH = 'switch' +TYPE_VALVE = 'valve' # #### Services #### SERV_ACCESSORY_INFO = 'AccessoryInformation' @@ -57,6 +61,7 @@ SERV_SMOKE_SENSOR = 'SmokeSensor' SERV_SWITCH = 'Switch' SERV_TEMPERATURE_SENSOR = 'TemperatureSensor' SERV_THERMOSTAT = 'Thermostat' +SERV_VALVE = 'Valve' SERV_WINDOW_COVERING = 'WindowCovering' # #### Characteristics #### @@ -85,6 +90,7 @@ CHAR_CURRENT_TEMPERATURE = 'CurrentTemperature' CHAR_FIRMWARE_REVISION = 'FirmwareRevision' CHAR_HEATING_THRESHOLD_TEMPERATURE = 'HeatingThresholdTemperature' CHAR_HUE = 'Hue' +CHAR_IN_USE = 'InUse' CHAR_LEAK_DETECTED = 'LeakDetected' CHAR_LOCK_CURRENT_STATE = 'LockCurrentState' CHAR_LOCK_TARGET_STATE = 'LockTargetState' @@ -109,6 +115,7 @@ CHAR_TARGET_POSITION = 'TargetPosition' CHAR_TARGET_SECURITY_STATE = 'SecuritySystemTargetState' CHAR_TARGET_TEMPERATURE = 'TargetTemperature' CHAR_TEMP_DISPLAY_UNITS = 'TemperatureDisplayUnits' +CHAR_VALVE_TYPE = 'ValveType' # #### Properties #### PROP_MAX_VALUE = 'maxValue' diff --git a/homeassistant/components/homekit/type_switches.py b/homeassistant/components/homekit/type_switches.py index a5724057eee..82a5d68d644 100644 --- a/homeassistant/components/homekit/type_switches.py +++ b/homeassistant/components/homekit/type_switches.py @@ -5,15 +5,29 @@ from pyhap.const import CATEGORY_OUTLET, CATEGORY_SWITCH from homeassistant.components.switch import DOMAIN from homeassistant.const import ( - ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF, STATE_ON) + ATTR_ENTITY_ID, CONF_TYPE, SERVICE_TURN_ON, SERVICE_TURN_OFF, STATE_ON) from homeassistant.core import split_entity_id from . import TYPES from .accessories import HomeAccessory -from .const import CHAR_ON, CHAR_OUTLET_IN_USE, SERV_OUTLET, SERV_SWITCH +from .const import ( + CHAR_ACTIVE, CHAR_IN_USE, CHAR_ON, CHAR_OUTLET_IN_USE, CHAR_VALVE_TYPE, + SERV_OUTLET, SERV_SWITCH, SERV_VALVE, TYPE_FAUCET, TYPE_SHOWER, + TYPE_SPRINKLER, TYPE_VALVE) _LOGGER = logging.getLogger(__name__) +CATEGORY_SPRINKLER = 28 +CATEGORY_FAUCET = 29 +CATEGORY_SHOWER_HEAD = 30 + +VALVE_TYPE = { + TYPE_FAUCET: (CATEGORY_FAUCET, 3), + TYPE_SHOWER: (CATEGORY_SHOWER_HEAD, 2), + TYPE_SPRINKLER: (CATEGORY_SPRINKLER, 1), + TYPE_VALVE: (CATEGORY_FAUCET, 0), +} + @TYPES.register('Outlet') class Outlet(HomeAccessory): @@ -80,3 +94,43 @@ class Switch(HomeAccessory): self.entity_id, current_state) self.char_on.set_value(current_state) self.flag_target_state = False + + +@TYPES.register('Valve') +class Valve(HomeAccessory): + """Generate a Valve accessory.""" + + def __init__(self, *args): + """Initialize a Valve accessory object.""" + super().__init__(*args) + self.flag_target_state = False + valve_type = self.config[CONF_TYPE] + self.category = VALVE_TYPE[valve_type][0] + + serv_valve = self.add_preload_service(SERV_VALVE) + self.char_active = serv_valve.configure_char( + CHAR_ACTIVE, value=False, setter_callback=self.set_state) + self.char_in_use = serv_valve.configure_char( + CHAR_IN_USE, value=False) + self.char_valve_type = serv_valve.configure_char( + CHAR_VALVE_TYPE, value=VALVE_TYPE[valve_type][1]) + + def set_state(self, value): + """Move value state to value if call came from HomeKit.""" + _LOGGER.debug('%s: Set switch state to %s', + self.entity_id, value) + self.flag_target_state = True + self.char_in_use.set_value(value) + params = {ATTR_ENTITY_ID: self.entity_id} + service = SERVICE_TURN_ON if value else SERVICE_TURN_OFF + self.hass.services.call(DOMAIN, service, params) + + def update_state(self, new_state): + """Update switch state after state changed.""" + current_state = (new_state.state == STATE_ON) + if not self.flag_target_state: + _LOGGER.debug('%s: Set current state to %s', + self.entity_id, current_state) + self.char_active.set_value(current_state) + self.char_in_use.set_value(current_state) + self.flag_target_state = False diff --git a/homeassistant/components/homekit/util.py b/homeassistant/components/homekit/util.py index 33a4bec321f..4dd7396cf8d 100644 --- a/homeassistant/components/homekit/util.py +++ b/homeassistant/components/homekit/util.py @@ -11,8 +11,8 @@ import homeassistant.helpers.config_validation as cv import homeassistant.util.temperature as temp_util from .const import ( CONF_FEATURE, CONF_FEATURE_LIST, HOMEKIT_NOTIFY_ID, FEATURE_ON_OFF, - FEATURE_PLAY_PAUSE, FEATURE_PLAY_STOP, FEATURE_TOGGLE_MUTE, TYPE_OUTLET, - TYPE_SWITCH) + FEATURE_PLAY_PAUSE, FEATURE_PLAY_STOP, FEATURE_TOGGLE_MUTE, TYPE_FAUCET, + TYPE_OUTLET, TYPE_SHOWER, TYPE_SPRINKLER, TYPE_SWITCH, TYPE_VALVE) _LOGGER = logging.getLogger(__name__) @@ -38,7 +38,9 @@ MEDIA_PLAYER_SCHEMA = vol.Schema({ SWITCH_TYPE_SCHEMA = BASIC_INFO_SCHEMA.extend({ vol.Optional(CONF_TYPE, default=TYPE_SWITCH): vol.All( - cv.string, vol.In((TYPE_OUTLET, TYPE_SWITCH))), + cv.string, vol.In(( + TYPE_FAUCET, TYPE_OUTLET, TYPE_SHOWER, TYPE_SPRINKLER, + TYPE_SWITCH, TYPE_VALVE))), }) diff --git a/tests/components/homekit/test_get_accessories.py b/tests/components/homekit/test_get_accessories.py index 5b76618d460..d5552cce82c 100644 --- a/tests/components/homekit/test_get_accessories.py +++ b/tests/components/homekit/test_get_accessories.py @@ -9,7 +9,8 @@ import homeassistant.components.climate as climate import homeassistant.components.media_player as media_player from homeassistant.components.homekit import get_accessory, TYPES from homeassistant.components.homekit.const import ( - CONF_FEATURE_LIST, FEATURE_ON_OFF, TYPE_OUTLET, TYPE_SWITCH) + CONF_FEATURE_LIST, FEATURE_ON_OFF, TYPE_FAUCET, TYPE_OUTLET, TYPE_SHOWER, + TYPE_SPRINKLER, TYPE_SWITCH, TYPE_VALVE) from homeassistant.const import ( ATTR_CODE, ATTR_DEVICE_CLASS, ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT, CONF_NAME, CONF_TYPE, TEMP_CELSIUS, @@ -140,6 +141,10 @@ def test_type_sensors(type_name, entity_id, state, attrs): ('Switch', 'script.test', 'on', {}, {}), ('Switch', 'switch.test', 'on', {}, {}), ('Switch', 'switch.test', 'on', {}, {CONF_TYPE: TYPE_SWITCH}), + ('Valve', 'switch.test', 'on', {}, {CONF_TYPE: TYPE_FAUCET}), + ('Valve', 'switch.test', 'on', {}, {CONF_TYPE: TYPE_VALVE}), + ('Valve', 'switch.test', 'on', {}, {CONF_TYPE: TYPE_SHOWER}), + ('Valve', 'switch.test', 'on', {}, {CONF_TYPE: TYPE_SPRINKLER}), ]) def test_type_switches(type_name, entity_id, state, attrs, config): """Test if switch types are associated correctly.""" diff --git a/tests/components/homekit/test_type_switches.py b/tests/components/homekit/test_type_switches.py index c2b80226508..bc44a93884a 100644 --- a/tests/components/homekit/test_type_switches.py +++ b/tests/components/homekit/test_type_switches.py @@ -1,8 +1,11 @@ """Test different accessory types: Switches.""" import pytest -from homeassistant.components.homekit.type_switches import Outlet, Switch -from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON +from homeassistant.components.homekit.const import ( + TYPE_FAUCET, TYPE_SHOWER, TYPE_SPRINKLER, TYPE_VALVE) +from homeassistant.components.homekit.type_switches import ( + Outlet, Switch, Valve) +from homeassistant.const import ATTR_ENTITY_ID, CONF_TYPE, STATE_OFF, STATE_ON from homeassistant.core import split_entity_id from tests.common import async_mock_service @@ -90,3 +93,70 @@ async def test_switch_set_state(hass, hk_driver, entity_id): await hass.async_block_till_done() assert call_turn_off assert call_turn_off[0].data[ATTR_ENTITY_ID] == entity_id + + +async def test_valve_set_state(hass, hk_driver): + """Test if Valve accessory and HA are updated accordingly.""" + entity_id = 'switch.valve_test' + + hass.states.async_set(entity_id, None) + await hass.async_block_till_done() + + acc = Valve(hass, hk_driver, 'Valve', entity_id, 2, + {CONF_TYPE: TYPE_FAUCET}) + await hass.async_add_job(acc.run) + await hass.async_block_till_done() + assert acc.category == 29 # Faucet + assert acc.char_valve_type.value == 3 # Water faucet + + acc = Valve(hass, hk_driver, 'Valve', entity_id, 2, + {CONF_TYPE: TYPE_SHOWER}) + await hass.async_add_job(acc.run) + await hass.async_block_till_done() + assert acc.category == 30 # Shower + assert acc.char_valve_type.value == 2 # Shower head + + acc = Valve(hass, hk_driver, 'Valve', entity_id, 2, + {CONF_TYPE: TYPE_SPRINKLER}) + await hass.async_add_job(acc.run) + await hass.async_block_till_done() + assert acc.category == 28 # Sprinkler + assert acc.char_valve_type.value == 1 # Irrigation + + acc = Valve(hass, hk_driver, 'Valve', entity_id, 2, + {CONF_TYPE: TYPE_VALVE}) + await hass.async_add_job(acc.run) + await hass.async_block_till_done() + + assert acc.aid == 2 + assert acc.category == 29 # Faucet + + assert acc.char_active.value is False + assert acc.char_in_use.value is False + assert acc.char_valve_type.value == 0 # Generic Valve + + hass.states.async_set(entity_id, STATE_ON) + await hass.async_block_till_done() + assert acc.char_active.value is True + assert acc.char_in_use.value is True + + hass.states.async_set(entity_id, STATE_OFF) + await hass.async_block_till_done() + assert acc.char_active.value is False + assert acc.char_in_use.value is False + + # Set from HomeKit + call_turn_on = async_mock_service(hass, 'switch', 'turn_on') + call_turn_off = async_mock_service(hass, 'switch', 'turn_off') + + await hass.async_add_job(acc.char_active.client_update_value, True) + await hass.async_block_till_done() + assert acc.char_in_use.value is True + assert call_turn_on + assert call_turn_on[0].data[ATTR_ENTITY_ID] == entity_id + + await hass.async_add_job(acc.char_active.client_update_value, False) + await hass.async_block_till_done() + assert acc.char_in_use.value is False + assert call_turn_off + assert call_turn_off[0].data[ATTR_ENTITY_ID] == entity_id diff --git a/tests/components/homekit/test_util.py b/tests/components/homekit/test_util.py index 6ae16ee8f33..0368dfa642e 100644 --- a/tests/components/homekit/test_util.py +++ b/tests/components/homekit/test_util.py @@ -4,7 +4,8 @@ import voluptuous as vol from homeassistant.components.homekit.const import ( CONF_FEATURE, CONF_FEATURE_LIST, HOMEKIT_NOTIFY_ID, FEATURE_ON_OFF, - FEATURE_PLAY_PAUSE, TYPE_OUTLET) + FEATURE_PLAY_PAUSE, TYPE_FAUCET, TYPE_OUTLET, TYPE_SHOWER, TYPE_SPRINKLER, + TYPE_SWITCH, TYPE_VALVE) from homeassistant.components.homekit.util import ( convert_to_float, density_to_air_quality, dismiss_setup_message, show_setup_message, temperature_to_homekit, temperature_to_states, @@ -58,8 +59,19 @@ def test_validate_entity_config(): assert vec({'media_player.demo': config}) == \ {'media_player.demo': {CONF_FEATURE_LIST: {FEATURE_ON_OFF: {}, FEATURE_PLAY_PAUSE: {}}}} + + assert vec({'switch.demo': {CONF_TYPE: TYPE_FAUCET}}) == \ + {'switch.demo': {CONF_TYPE: TYPE_FAUCET}} assert vec({'switch.demo': {CONF_TYPE: TYPE_OUTLET}}) == \ {'switch.demo': {CONF_TYPE: TYPE_OUTLET}} + assert vec({'switch.demo': {CONF_TYPE: TYPE_SHOWER}}) == \ + {'switch.demo': {CONF_TYPE: TYPE_SHOWER}} + assert vec({'switch.demo': {CONF_TYPE: TYPE_SPRINKLER}}) == \ + {'switch.demo': {CONF_TYPE: TYPE_SPRINKLER}} + assert vec({'switch.demo': {CONF_TYPE: TYPE_SWITCH}}) == \ + {'switch.demo': {CONF_TYPE: TYPE_SWITCH}} + assert vec({'switch.demo': {CONF_TYPE: TYPE_VALVE}}) == \ + {'switch.demo': {CONF_TYPE: TYPE_VALVE}} def test_validate_media_player_features(): From 29c3f1618f222470ea9741c960ca58cb7698c354 Mon Sep 17 00:00:00 2001 From: Paul Annekov Date: Fri, 5 Oct 2018 14:33:28 +0300 Subject: [PATCH 205/247] Fix miflora connection errors during platform setup (#16798) * possible fix for startup delay * fixed reported issues * moved update code into setup * reverted to previous solution --- homeassistant/components/sensor/miflora.py | 24 ++++++++++------------ 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/sensor/miflora.py b/homeassistant/components/sensor/miflora.py index 6f0fb3aba30..a097974fbfd 100644 --- a/homeassistant/components/sensor/miflora.py +++ b/homeassistant/components/sensor/miflora.py @@ -4,20 +4,17 @@ Support for Xiaomi Mi Flora BLE plant sensor. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.miflora/ """ -import asyncio from datetime import timedelta import logging import voluptuous as vol -import async_timeout from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv -from homeassistant.exceptions import PlatformNotReady from homeassistant.const import ( CONF_FORCE_UPDATE, CONF_MONITORED_CONDITIONS, CONF_NAME, CONF_MAC, - CONF_SCAN_INTERVAL) - + CONF_SCAN_INTERVAL, EVENT_HOMEASSISTANT_START) +from homeassistant.core import callback REQUIREMENTS = ['miflora==0.4.0'] @@ -75,13 +72,6 @@ async def async_setup_platform(hass, config, async_add_entities, devs = [] - try: - with async_timeout.timeout(9): - await hass.async_add_executor_job(poller.fill_cache) - except asyncio.TimeoutError: - _LOGGER.error('Unable to connect to %s', config.get(CONF_MAC)) - raise PlatformNotReady - for parameter in config[CONF_MONITORED_CONDITIONS]: name = SENSOR_TYPES[parameter][0] unit = SENSOR_TYPES[parameter][1] @@ -93,7 +83,7 @@ async def async_setup_platform(hass, config, async_add_entities, devs.append(MiFloraSensor( poller, parameter, name, unit, force_update, median)) - async_add_entities(devs, update_before_add=True) + async_add_entities(devs) class MiFloraSensor(Entity): @@ -113,6 +103,14 @@ class MiFloraSensor(Entity): # Use median_count = 1 if no filtering is required. self.median_count = median + async def async_added_to_hass(self): + """Set initial state.""" + @callback + def on_startup(): + self.async_schedule_update_ha_state(True) + + self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, on_startup) + @property def name(self): """Return the name of the sensor.""" From 13aa93514797b05fb53785622a262cabc7e787a8 Mon Sep 17 00:00:00 2001 From: prophit987 <37056396+prophit987@users.noreply.github.com> Date: Fri, 5 Oct 2018 05:39:10 -0700 Subject: [PATCH 206/247] Discover Danfoss/devolo RS Room Sensor thermostat (#17153) Allows the Danfoss/Devolo RS Room Sensor thermostat to be discovered as climate device in homeassistant. This device is defined as GENERIC_TYPE_MULTILEVEL_SENSOR, and not GENERIC_TYPE_THERMOSTAT. Ref: https://community.home-assistant.io/t/devolo-zwave-thermostat/58739 From d1636dab318bcb133849e7d85e1334e411129b57 Mon Sep 17 00:00:00 2001 From: prophit987 <37056396+prophit987@users.noreply.github.com> Date: Fri, 5 Oct 2018 05:55:16 -0700 Subject: [PATCH 207/247] @danielhiversen as codeowner for Tibber (#17154) --- CODEOWNERS | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CODEOWNERS b/CODEOWNERS index 00ca296c1d2..91d5fd67670 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -82,7 +82,6 @@ homeassistant/components/sensor/qnap.py @colinodell homeassistant/components/sensor/sma.py @kellerza homeassistant/components/sensor/sql.py @dgomes homeassistant/components/sensor/sytadin.py @gautric -homeassistant/components/sensor/tibber.py @danielhiversen homeassistant/components/sensor/waqi.py @andrey-git homeassistant/components/switch/tplink.py @rytilahti homeassistant/components/vacuum/roomba.py @pschmitt @@ -124,6 +123,8 @@ homeassistant/components/tesla.py @zabuldon homeassistant/components/*/tesla.py @zabuldon homeassistant/components/tellduslive.py @molobrakos @fredrike homeassistant/components/*/tellduslive.py @molobrakos @fredrike +homeassistant/components/tibber/* @danielhiversen +homeassistant/components/*/tibber.py @danielhiversen homeassistant/components/*/tradfri.py @ggravlingen homeassistant/components/upcloud.py @scop homeassistant/components/*/upcloud.py @scop From 92e28067d8f358279ba86f83c482ac464fe7be5c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 5 Oct 2018 17:49:27 +0200 Subject: [PATCH 208/247] Update frontend to 20181005.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 fffb3086d6d..9a5e7c05a56 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==20181002.0'] +REQUIREMENTS = ['home-assistant-frontend==20181005.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log', diff --git a/requirements_all.txt b/requirements_all.txt index 25f0eeac147..faadd0f5016 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -458,7 +458,7 @@ hole==0.3.0 holidays==0.9.7 # homeassistant.components.frontend -home-assistant-frontend==20181002.0 +home-assistant-frontend==20181005.0 # homeassistant.components.homekit_controller # homekit==0.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4f27664e7df..55b8733fff1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -90,7 +90,7 @@ hdate==0.6.3 holidays==0.9.7 # homeassistant.components.frontend -home-assistant-frontend==20181002.0 +home-assistant-frontend==20181005.0 # homeassistant.components.homematicip_cloud homematicip==0.9.8 From 0c770520ed67513ea663c93c1682baa7e0e8b3d1 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 5 Oct 2018 17:50:00 +0200 Subject: [PATCH 209/247] Update translations --- .../components/auth/.translations/ko.json | 2 +- .../auth/.translations/zh-Hant.json | 2 +- .../components/hangouts/.translations/de.json | 2 ++ .../components/hue/.translations/ko.json | 4 +-- .../components/ifttt/.translations/de.json | 3 ++ .../components/ifttt/.translations/hu.json | 6 ++++ .../components/ifttt/.translations/ko.json | 6 ++-- .../components/ifttt/.translations/sv.json | 5 ++++ .../ifttt/.translations/zh-Hans.json | 4 +-- .../components/mqtt/.translations/de.json | 7 +++++ .../components/mqtt/.translations/es-419.json | 9 ++++++ .../components/mqtt/.translations/es.json | 9 ++++++ .../components/mqtt/.translations/hu.json | 6 ++++ .../components/mqtt/.translations/ko.json | 4 +-- .../components/mqtt/.translations/nl.json | 7 +++++ .../components/mqtt/.translations/ru.json | 7 +++++ .../mqtt/.translations/zh-Hans.json | 7 +++++ .../mqtt/.translations/zh-Hant.json | 9 +++++- .../components/upnp/.translations/ca.json | 23 ++++++++++++++ .../components/upnp/.translations/de.json | 23 ++++++++++++++ .../components/upnp/.translations/ko.json | 26 ++++++++++++++++ .../components/upnp/.translations/lb.json | 27 +++++++++++++++++ .../components/upnp/.translations/nl.json | 30 ++++++++----------- .../components/upnp/.translations/pl.json | 29 ++++++++++++++++++ .../components/upnp/.translations/ru.json | 23 ++++++++++++++ .../upnp/.translations/zh-Hans.json | 23 ++++++++++++++ .../upnp/.translations/zh-Hant.json | 23 ++++++++++++++ 27 files changed, 296 insertions(+), 30 deletions(-) create mode 100644 homeassistant/components/ifttt/.translations/sv.json create mode 100644 homeassistant/components/mqtt/.translations/es-419.json create mode 100644 homeassistant/components/mqtt/.translations/es.json create mode 100644 homeassistant/components/upnp/.translations/ca.json create mode 100644 homeassistant/components/upnp/.translations/de.json create mode 100644 homeassistant/components/upnp/.translations/ko.json create mode 100644 homeassistant/components/upnp/.translations/lb.json create mode 100644 homeassistant/components/upnp/.translations/pl.json create mode 100644 homeassistant/components/upnp/.translations/ru.json create mode 100644 homeassistant/components/upnp/.translations/zh-Hans.json create mode 100644 homeassistant/components/upnp/.translations/zh-Hant.json diff --git a/homeassistant/components/auth/.translations/ko.json b/homeassistant/components/auth/.translations/ko.json index e1f26e88bc7..7efc50d534c 100644 --- a/homeassistant/components/auth/.translations/ko.json +++ b/homeassistant/components/auth/.translations/ko.json @@ -26,7 +26,7 @@ "step": { "init": { "description": "\uc2dc\uac04 \uae30\ubc18\uc758 \uc77c\ud68c\uc6a9 \ube44\ubc00\ubc88\ud638\ub97c \uc0ac\uc6a9\ud558\ub294 2\ub2e8\uacc4 \uc778\uc99d\uc744 \ud558\ub824\uba74 \uc778\uc99d\uc6a9 \uc571\uc744 \uc774\uc6a9\ud574\uc11c QR \ucf54\ub4dc\ub97c \uc2a4\uce94\ud574 \uc8fc\uc138\uc694. \uc778\uc99d\uc6a9 \uc571\uc740 [Google OTP](https://support.google.com/accounts/answer/1066447) \ub610\ub294 [Authy](https://authy.com/) \ub97c \ucd94\ucc9c\ub4dc\ub9bd\ub2c8\ub2e4.\n\n{qr_code}\n\n\uc2a4\uce94 \ud6c4\uc5d0 \uc0dd\uc131\ub41c 6\uc790\ub9ac \ucf54\ub4dc\ub97c \uc785\ub825\ud574\uc11c \uc124\uc815\uc744 \ud655\uc778\ud558\uc138\uc694. QR \ucf54\ub4dc \uc2a4\uce94\uc5d0 \ubb38\uc81c\uac00 \uc788\ub2e4\uba74, **`{code}`** \ucf54\ub4dc\ub85c \uc9c1\uc811 \uc124\uc815\ud574\ubcf4\uc138\uc694.", - "title": "TOTP \ub97c \uc0ac\uc6a9\ud558\uc5ec 2 \ub2e8\uacc4 \uc778\uc99d \uad6c\uc131" + "title": "TOTP \ub97c \uc0ac\uc6a9\ud558\uc5ec 2\ub2e8\uacc4 \uc778\uc99d \uad6c\uc131" } }, "title": "TOTP (\uc2dc\uac04 \uae30\ubc18 OTP)" diff --git a/homeassistant/components/auth/.translations/zh-Hant.json b/homeassistant/components/auth/.translations/zh-Hant.json index e791f20a738..b7a26f5079c 100644 --- a/homeassistant/components/auth/.translations/zh-Hant.json +++ b/homeassistant/components/auth/.translations/zh-Hant.json @@ -25,7 +25,7 @@ }, "step": { "init": { - "description": "\u6b32\u555f\u7528\u4e00\u6b21\u6027\u4e14\u5177\u6642\u6548\u6027\u7684\u5bc6\u78bc\u4e4b\u5169\u6b65\u9a5f\u9a57\u8b49\u529f\u80fd\uff0c\u8acb\u4f7f\u7528\u60a8\u7684\u9a57\u8b49 App \u6383\u7784\u4e0b\u65b9\u7684 QR code \u3002\u5018\u82e5\u60a8\u5c1a\u672a\u5b89\u88dd\u4efb\u4f55 App\uff0c\u63a8\u85a6\u60a8\u4f7f\u7528 [Google Authenticator](https://support.google.com/accounts/answer/1066447) \u6216 [Authy](https://authy.com/)\u3002\n\n{qr_code}\n\n\u65bc\u6383\u63cf\u4e4b\u5f8c\uff0c\u8f38\u5165 App \u4e2d\u7684\u516d\u4f4d\u6578\u5b57\u9032\u884c\u8a2d\u5b9a\u9a57\u8b49\u3002\u5047\u5982\u6383\u63cf\u51fa\u73fe\u554f\u984c\uff0c\u8acb\u624b\u52d5\u8f38\u5165\u4ee5\u4e0b\u9a57\u8b49\u78bc **`{code}`**\u3002", + "description": "\u6b32\u555f\u7528\u4e00\u6b21\u6027\u4e14\u5177\u6642\u6548\u6027\u7684\u5bc6\u78bc\u4e4b\u5169\u6b65\u9a5f\u9a57\u8b49\u529f\u80fd\uff0c\u8acb\u4f7f\u7528\u60a8\u7684\u9a57\u8b49 App \u6383\u7784\u4e0b\u65b9\u7684 QR code \u3002\u5018\u82e5\u60a8\u5c1a\u672a\u5b89\u88dd\u4efb\u4f55 App\uff0c\u63a8\u85a6\u60a8\u4f7f\u7528 [Google Authenticator](https://support.google.com/accounts/answer/1066447) \u6216 [Authy](https://authy.com/)\u3002\n\n{qr_code}\n\n\u65bc\u641c\u5c0b\u4e4b\u5f8c\uff0c\u8f38\u5165 App \u4e2d\u7684\u516d\u4f4d\u6578\u5b57\u9032\u884c\u8a2d\u5b9a\u9a57\u8b49\u3002\u5047\u5982\u641c\u5c0b\u51fa\u73fe\u554f\u984c\uff0c\u8acb\u624b\u52d5\u8f38\u5165\u4ee5\u4e0b\u9a57\u8b49\u78bc **`{code}`**\u3002", "title": "\u4f7f\u7528 TOTP \u8a2d\u5b9a\u5169\u6b65\u9a5f\u9a57\u8b49" } }, diff --git a/homeassistant/components/hangouts/.translations/de.json b/homeassistant/components/hangouts/.translations/de.json index a2ed8d21230..e0f18b6cccf 100644 --- a/homeassistant/components/hangouts/.translations/de.json +++ b/homeassistant/components/hangouts/.translations/de.json @@ -14,6 +14,7 @@ "data": { "2fa": "2FA PIN" }, + "description": "Leer", "title": "2-Faktor-Authentifizierung" }, "user": { @@ -21,6 +22,7 @@ "email": "E-Mail-Adresse", "password": "Passwort" }, + "description": "Leer", "title": "Google Hangouts Login" } }, diff --git a/homeassistant/components/hue/.translations/ko.json b/homeassistant/components/hue/.translations/ko.json index 48fe4e8af28..a4a8051663e 100644 --- a/homeassistant/components/hue/.translations/ko.json +++ b/homeassistant/components/hue/.translations/ko.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "all_configured": "\ubaa8\ub4e0 \ud544\ub9bd\uc2a4 Hue \ube0c\ub9bf\uc9c0\uac00 \uc774\ubbf8 \uc124\uc815\ub41c \uc0c1\ud0dc\uc785\ub2c8\ub2e4", - "already_configured": "\ube0c\ub9bf\uc9c0\uac00 \uc774\ubbf8 \uc124\uc815\ub41c \uc0c1\ud0dc\uc785\ub2c8\ub2e4", + "all_configured": "\ubaa8\ub4e0 \ud544\ub9bd\uc2a4 Hue \ube0c\ub9bf\uc9c0\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "already_configured": "\ube0c\ub9bf\uc9c0\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "cannot_connect": "\ube0c\ub9ac\uc9c0\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", "discover_timeout": "Hue \ube0c\ub9bf\uc9c0\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", "no_bridges": "\ubc1c\uacac\ub41c \ud544\ub9bd\uc2a4 Hue \ube0c\ub9bf\uc9c0\uac00 \uc5c6\uc2b5\ub2c8\ub2e4", diff --git a/homeassistant/components/ifttt/.translations/de.json b/homeassistant/components/ifttt/.translations/de.json index da2cec98c72..b8fdc819753 100644 --- a/homeassistant/components/ifttt/.translations/de.json +++ b/homeassistant/components/ifttt/.translations/de.json @@ -4,6 +4,9 @@ "not_internet_accessible": "Auf Ihre Home Assistant-Instanz muss vom Internet aus zugegriffen werden k\u00f6nnen, um IFTTT-Nachrichten zu empfangen.", "one_instance_allowed": "Nur eine einzige Instanz ist notwendig." }, + "create_entry": { + "default": "Um Ereignisse an den Home Assistant zu senden, m\u00fcssen Sie die Aktion \"Eine Webanforderung erstellen\" aus dem [IFTTT Webhook Applet]({applet_url}) ausw\u00e4hlen.\n\nF\u00fcllen Sie folgende Informationen aus: \n- URL: `{webhook_url}`\n- Methode: POST\n- Inhaltstyp: application/json\n\nIn der Dokumentation ({docs_url}) finden Sie Informationen zur Konfiguration der Automation eingehender Daten." + }, "step": { "user": { "description": "Bist du sicher, dass du IFTTT einrichten m\u00f6chtest?", diff --git a/homeassistant/components/ifttt/.translations/hu.json b/homeassistant/components/ifttt/.translations/hu.json index 077956287b3..a131f848d45 100644 --- a/homeassistant/components/ifttt/.translations/hu.json +++ b/homeassistant/components/ifttt/.translations/hu.json @@ -1,5 +1,11 @@ { "config": { + "step": { + "user": { + "description": "Biztosan be szeretn\u00e9d \u00e1ll\u00edtani az IFTTT-t?", + "title": "IFTTT Webhook Applet be\u00e1ll\u00edt\u00e1sa" + } + }, "title": "IFTTT" } } \ No newline at end of file diff --git a/homeassistant/components/ifttt/.translations/ko.json b/homeassistant/components/ifttt/.translations/ko.json index 1a53480e1b1..832123d5065 100644 --- a/homeassistant/components/ifttt/.translations/ko.json +++ b/homeassistant/components/ifttt/.translations/ko.json @@ -1,16 +1,16 @@ { "config": { "abort": { - "not_internet_accessible": "IFTTT \uba54\uc2dc\uc9c0\ub97c \ubc1b\uc73c\ub824\uba74 \uc778\ud130\ub137\uc5d0\uc11c \ud648 \uc5b4\uc2dc\uc2a4\ud134\ud2b8 \uc778\uc2a4\ud134\uc2a4\uc5d0 \uc561\uc138\uc2a4 \ud560 \uc218 \uc788\uc5b4\uc57c \ud569\ub2c8\ub2e4.", + "not_internet_accessible": "IFTTT \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\uae30 \uc704\ud574\uc11c\ub294 [IFTTT Webhook \uc560\ud50c\ub9bf]({applet_url}) \uc5d0\uc11c \"Make a web request\" \uc791\uc5c5\uc744 \uc0ac\uc6a9\ud574\uc57c \ud569\ub2c8\ub2e4. \n\n \ub2e4\uc74c\uc758 \uc815\ubcf4\ub97c \uc785\ub825\ud574 \uc8fc\uc138\uc694.\n\n - URL: `{webhook_url}` \n - Method: POST \n - Content Type: application/json \n\n Home Assistant \ub85c \ub4e4\uc5b4\uc624\ub294 \ub370\uc774\ud130\ub97c \ucc98\ub9ac\ud558\uae30 \uc704\ud55c \uc790\ub3d9\ud654\ub97c \uad6c\uc131\ud558\ub294 \ubc29\ubc95\uc740 [\ubcf8 \ubb38\uc11c]({docs_url}) \ub97c \ucc38\uc870\ud574 \uc8fc\uc138\uc694." + "default": "Home Assistant \ub85c \uc774\ubca4\ud2b8\ub97c \ubcf4\ub0b4\uae30 \uc704\ud574\uc11c\ub294 [IFTTT Webhook \uc560\ud50c\ub9bf]({applet_url}) \uc5d0\uc11c \"Make a web request\" \ub97c \uc0ac\uc6a9\ud574\uc57c \ud569\ub2c8\ub2e4. \n\n \ub2e4\uc74c\uc758 \uc815\ubcf4\ub97c \uc785\ub825\ud574 \uc8fc\uc138\uc694.\n\n - URL: `{webhook_url}` \n - Method: POST \n - Content Type: application/json \n\n Home Assistant \ub85c \ub4e4\uc5b4\uc624\ub294 \ub370\uc774\ud130\ub97c \ucc98\ub9ac\ud558\uae30 \uc704\ud55c \uc790\ub3d9\ud654\ub97c \uad6c\uc131\ud558\ub294 \ubc29\ubc95\uc740 [\ubcf8 \ubb38\uc11c]({docs_url}) \ub97c \ucc38\uc870\ud574 \uc8fc\uc138\uc694." }, "step": { "user": { "description": "IFTTT \ub97c \uc124\uc815 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", - "title": "IFTTT Webhook Applet \uc124\uc815" + "title": "IFTTT Webhook \uc560\ud50c\ub9bf \uc124\uc815" } }, "title": "IFTTT" diff --git a/homeassistant/components/ifttt/.translations/sv.json b/homeassistant/components/ifttt/.translations/sv.json new file mode 100644 index 00000000000..077956287b3 --- /dev/null +++ b/homeassistant/components/ifttt/.translations/sv.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "IFTTT" + } +} \ No newline at end of file diff --git a/homeassistant/components/ifttt/.translations/zh-Hans.json b/homeassistant/components/ifttt/.translations/zh-Hans.json index 66e667d0e47..e9f7aeb36d4 100644 --- a/homeassistant/components/ifttt/.translations/zh-Hans.json +++ b/homeassistant/components/ifttt/.translations/zh-Hans.json @@ -5,12 +5,12 @@ "one_instance_allowed": "\u53ea\u6709\u4e00\u4e2a\u5b9e\u4f8b\u662f\u5fc5\u9700\u7684\u3002" }, "create_entry": { - "default": "\u8981\u5411 Home Assistant \u53d1\u9001\u4e8b\u4ef6\uff0c\u60a8\u9700\u8981\u4f7f\u7528 [IFTTT Webhook \u5c0f\u7a0b\u5e8f]({applet_url}) \u4e2d\u7684 \"Make a web request\" \u52a8\u4f5c\u3002\n\n\u586b\u5199\u4ee5\u4e0b\u4fe1\u606f\uff1a\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\n\u6709\u5173\u5982\u4f55\u914d\u7f6e\u81ea\u52a8\u5316\u4ee5\u5904\u7406\u4f20\u5165\u7684\u6570\u636e\uff0c\u8bf7\u53c2\u9605[\u6587\u6863]({docs_url})\u3002" + "default": "\u8981\u5411 Home Assistant \u53d1\u9001\u4e8b\u4ef6\uff0c\u60a8\u9700\u8981\u4f7f\u7528 [IFTTT Webhook applet]({applet_url}) \u4e2d\u7684 \"Make a web request\" \u52a8\u4f5c\u3002\n\n\u586b\u5199\u4ee5\u4e0b\u4fe1\u606f\uff1a\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\n\u6709\u5173\u5982\u4f55\u914d\u7f6e\u81ea\u52a8\u5316\u4ee5\u5904\u7406\u4f20\u5165\u7684\u6570\u636e\uff0c\u8bf7\u53c2\u9605[\u6587\u6863]({docs_url})\u3002" }, "step": { "user": { "description": "\u60a8\u786e\u5b9a\u8981\u8bbe\u7f6e IFTTT \u5417\uff1f", - "title": "\u8bbe\u7f6e IFTTT Webhook \u5c0f\u7a0b\u5e8f" + "title": "\u8bbe\u7f6e IFTTT Webhook Applet" } }, "title": "IFTTT" diff --git a/homeassistant/components/mqtt/.translations/de.json b/homeassistant/components/mqtt/.translations/de.json index f007e23bb35..1c895136d9d 100644 --- a/homeassistant/components/mqtt/.translations/de.json +++ b/homeassistant/components/mqtt/.translations/de.json @@ -17,6 +17,13 @@ }, "description": "Bitte gib die Verbindungsinformationen deines MQTT-Brokers ein.", "title": "MQTT" + }, + "hassio_confirm": { + "data": { + "discovery": "Suche aktivieren" + }, + "description": "M\u00f6chten Sie den Home Assistant so konfigurieren, dass er eine Verbindung mit dem MQTT-Broker herstellt, der vom Add-on hass.io {addon} bereitgestellt wird?", + "title": "MQTT Broker per Hass.io add-on" } }, "title": "MQTT" diff --git a/homeassistant/components/mqtt/.translations/es-419.json b/homeassistant/components/mqtt/.translations/es-419.json new file mode 100644 index 00000000000..e9e869ae966 --- /dev/null +++ b/homeassistant/components/mqtt/.translations/es-419.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "hassio_confirm": { + "title": "MQTT Broker a trav\u00e9s del complemento Hass.io" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mqtt/.translations/es.json b/homeassistant/components/mqtt/.translations/es.json new file mode 100644 index 00000000000..e9e869ae966 --- /dev/null +++ b/homeassistant/components/mqtt/.translations/es.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "hassio_confirm": { + "title": "MQTT Broker a trav\u00e9s del complemento Hass.io" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mqtt/.translations/hu.json b/homeassistant/components/mqtt/.translations/hu.json index d85814e917c..ba08d36d581 100644 --- a/homeassistant/components/mqtt/.translations/hu.json +++ b/homeassistant/components/mqtt/.translations/hu.json @@ -16,6 +16,12 @@ }, "description": "K\u00e9rlek, add meg az MQTT br\u00f3ker kapcsol\u00f3d\u00e1si adatait.", "title": "MQTT" + }, + "hassio_confirm": { + "data": { + "discovery": "Felfedez\u00e9s enged\u00e9lyez\u00e9se" + }, + "description": "Szeretn\u00e9d, hogy a Home Assistant csatlakozzon a hass.io addon {addon} \u00e1ltal biztos\u00edtott MQTT br\u00f3kerhez?" } }, "title": "MQTT" diff --git a/homeassistant/components/mqtt/.translations/ko.json b/homeassistant/components/mqtt/.translations/ko.json index 571d01b9884..ed552c0d994 100644 --- a/homeassistant/components/mqtt/.translations/ko.json +++ b/homeassistant/components/mqtt/.translations/ko.json @@ -15,7 +15,7 @@ "port": "\ud3ec\ud2b8", "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" }, - "description": "MQTT \ube0c\ub85c\ucee4\uc640\uc758 \uc5f0\uacb0 \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694.", + "description": "MQTT \ube0c\ub85c\ucee4\uc758 \uc5f0\uacb0 \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694.", "title": "MQTT" }, "hassio_confirm": { @@ -23,7 +23,7 @@ "discovery": "\uae30\uae30 \uac80\uc0c9 \ud65c\uc131\ud654" }, "description": "Hass.io \uc560\ub4dc\uc628 {addon} \uc5d0\uc11c \uc81c\uacf5\ud558\ub294 MQTT \ube0c\ub85c\ucee4\uc5d0 \uc5f0\uacb0\ud558\ub3c4\ub85d Home Assistant \ub97c \uad6c\uc131 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", - "title": "Hass.io \uc560\ub4dc\uc628\uc744 \ud1b5\ud55c MQTT \ube0c\ub85c\ucee4" + "title": "Hass.io \uc560\ub4dc\uc628\uc758 MQTT \ube0c\ub85c\ucee4" } }, "title": "MQTT" diff --git a/homeassistant/components/mqtt/.translations/nl.json b/homeassistant/components/mqtt/.translations/nl.json index e66a4944fcc..247755d8e89 100644 --- a/homeassistant/components/mqtt/.translations/nl.json +++ b/homeassistant/components/mqtt/.translations/nl.json @@ -17,6 +17,13 @@ }, "description": "MQTT", "title": "MQTT" + }, + "hassio_confirm": { + "data": { + "discovery": "Detectie inschakelen" + }, + "description": "Wilt u Home Assistant configureren om verbinding te maken met de MQTT-broker die wordt aangeboden door de hass.io add-on {addon} ?", + "title": "MQTTT Broker via Hass.io add-on" } }, "title": "MQTT" diff --git a/homeassistant/components/mqtt/.translations/ru.json b/homeassistant/components/mqtt/.translations/ru.json index 745fad31043..8b0ed27f3ae 100644 --- a/homeassistant/components/mqtt/.translations/ru.json +++ b/homeassistant/components/mqtt/.translations/ru.json @@ -17,6 +17,13 @@ }, "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u043e \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438 \u043a \u0432\u0430\u0448\u0435\u043c\u0443 \u0431\u0440\u043e\u043a\u0435\u0440\u0443 MQTT.", "title": "MQTT" + }, + "hassio_confirm": { + "data": { + "discovery": "\u0420\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u044c \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u0435" + }, + "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 Home Assistant \u0434\u043b\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u0431\u0440\u043e\u043a\u0435\u0440\u0443 MQTT \u0447\u0435\u0440\u0435\u0437 \u0430\u0434\u0434\u043e\u043d Hass.io {addon}?", + "title": "\u0411\u0440\u043e\u043a\u0435\u0440 MQTT \u0447\u0435\u0440\u0435\u0437 \u0430\u0434\u0434\u043e\u043d Hass.io" } }, "title": "MQTT" diff --git a/homeassistant/components/mqtt/.translations/zh-Hans.json b/homeassistant/components/mqtt/.translations/zh-Hans.json index 98a7d9eb4be..f30e1bf10b4 100644 --- a/homeassistant/components/mqtt/.translations/zh-Hans.json +++ b/homeassistant/components/mqtt/.translations/zh-Hans.json @@ -17,6 +17,13 @@ }, "description": "\u8bf7\u8f93\u5165\u60a8\u7684 MQTT \u670d\u52a1\u5668\u7684\u8fde\u63a5\u4fe1\u606f\u3002", "title": "MQTT" + }, + "hassio_confirm": { + "data": { + "discovery": "\u542f\u7528\u53d1\u73b0" + }, + "description": "\u662f\u5426\u8981\u914d\u7f6e Home Assistant \u8fde\u63a5\u5230 Hass.io \u52a0\u8f7d\u9879 {addon} \u63d0\u4f9b\u7684 MQTT \u670d\u52a1\u5668\uff1f", + "title": "\u6765\u81ea Hass.io \u52a0\u8f7d\u9879\u7684 MQTT \u670d\u52a1\u5668" } }, "title": "MQTT" diff --git a/homeassistant/components/mqtt/.translations/zh-Hant.json b/homeassistant/components/mqtt/.translations/zh-Hant.json index cf87ceb8f98..535ed848793 100644 --- a/homeassistant/components/mqtt/.translations/zh-Hant.json +++ b/homeassistant/components/mqtt/.translations/zh-Hant.json @@ -10,13 +10,20 @@ "broker": { "data": { "broker": "Broker", - "discovery": "\u958b\u555f\u63a2\u7d22", + "discovery": "\u958b\u555f\u641c\u5c0b", "password": "\u4f7f\u7528\u8005\u5bc6\u78bc", "port": "\u901a\u8a0a\u57e0", "username": "\u4f7f\u7528\u8005\u540d\u7a31" }, "description": "\u8acb\u8f38\u5165 MQTT Broker \u9023\u7dda\u8cc7\u8a0a\u3002", "title": "MQTT" + }, + "hassio_confirm": { + "data": { + "discovery": "\u958b\u555f\u641c\u5c0b" + }, + "description": "\u662f\u5426\u8981\u8a2d\u5b9a Home Assistant \u4ee5\u9023\u7dda\u81f3 Hass.io \u9644\u52a0\u7d44\u4ef6 {addon} \u4e4b MQTT broker\uff1f", + "title": "\u4f7f\u7528 Hass.io \u9644\u52a0\u7d44\u4ef6 MQTT Broker" } }, "title": "MQTT" diff --git a/homeassistant/components/upnp/.translations/ca.json b/homeassistant/components/upnp/.translations/ca.json new file mode 100644 index 00000000000..5dba9d1e16e --- /dev/null +++ b/homeassistant/components/upnp/.translations/ca.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "UPnP/IGD ja est\u00e0 configurat", + "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" + }, + "step": { + "init": { + "title": "UPnP/IGD" + }, + "user": { + "data": { + "enable_port_mapping": "Activa l'assignaci\u00f3 de ports per a Home Assistant", + "enable_sensors": "Afegiu sensors de tr\u00e0nsit", + "igd": "UPnP/IGD" + }, + "title": "Opcions de configuraci\u00f3 per a UPnP/IGD" + } + }, + "title": "UPnP/IGD" + } +} \ No newline at end of file diff --git a/homeassistant/components/upnp/.translations/de.json b/homeassistant/components/upnp/.translations/de.json new file mode 100644 index 00000000000..675d1eb7d0c --- /dev/null +++ b/homeassistant/components/upnp/.translations/de.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "UPnP/IGD ist bereits konfiguriert", + "no_devices_discovered": "Keine UPnP/IGDs entdeckt", + "no_sensors_or_port_mapping": "Aktiviere mindestens Sensoren oder Port-Mapping" + }, + "step": { + "init": { + "title": "UPnP/IGD" + }, + "user": { + "data": { + "enable_port_mapping": "Aktiviere Port-Mapping f\u00fcr Home Assistant", + "enable_sensors": "Verkehrssensoren hinzuf\u00fcgen", + "igd": "UPnP/IGD" + }, + "title": "Konfigurationsoptionen f\u00fcr UPnP/IGD" + } + }, + "title": "UPnP/IGD" + } +} \ No newline at end of file diff --git a/homeassistant/components/upnp/.translations/ko.json b/homeassistant/components/upnp/.translations/ko.json new file mode 100644 index 00000000000..4da5bedb9b3 --- /dev/null +++ b/homeassistant/components/upnp/.translations/ko.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "UPnP/IGD \uac00 \uc774\ubbf8 \uc124\uc815\ub41c \uc0c1\ud0dc\uc785\ub2c8\ub2e4", + "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" + }, + "error": { + "other": "\uc5d0\ub7ec" + }, + "step": { + "init": { + "title": "UPnP/IGD" + }, + "user": { + "data": { + "enable_port_mapping": "Home Assistant \ud3ec\ud2b8 \ub9e4\ud551 \ud65c\uc131\ud654", + "enable_sensors": "\ud2b8\ub798\ud53d \uc13c\uc11c \ucd94\uac00", + "igd": "UPnP/IGD" + }, + "title": "UPnP/IGD \uc758 \uad6c\uc131 \uc635\uc158" + } + }, + "title": "UPnP/IGD" + } +} \ No newline at end of file diff --git a/homeassistant/components/upnp/.translations/lb.json b/homeassistant/components/upnp/.translations/lb.json new file mode 100644 index 00000000000..1d13492a487 --- /dev/null +++ b/homeassistant/components/upnp/.translations/lb.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "UPnP/IGD ass scho konfigur\u00e9iert", + "no_devices_discovered": "Keng UPnP/IGDs entdeckt", + "no_sensors_or_port_mapping": "Aktiv\u00e9ier op mannst Sensoren oder Port Mapping" + }, + "error": { + "one": "Een", + "other": "Aaner" + }, + "step": { + "init": { + "title": "UPnP/IGD" + }, + "user": { + "data": { + "enable_port_mapping": "Port Mapping fir Home Assistant aktiv\u00e9ieren", + "enable_sensors": "Trafic Sensoren dob\u00e4isetzen", + "igd": "UPnP/IGD" + }, + "title": "Konfiguratiouns Optiounen fir UPnP/IGD" + } + }, + "title": "UPnP/IGD" + } +} \ No newline at end of file diff --git a/homeassistant/components/upnp/.translations/nl.json b/homeassistant/components/upnp/.translations/nl.json index 55a94a8ea6f..c6356709170 100644 --- a/homeassistant/components/upnp/.translations/nl.json +++ b/homeassistant/components/upnp/.translations/nl.json @@ -1,25 +1,19 @@ { "config": { - "title": "UPnP/IGD", + "abort": { + "already_configured": "UPnP / IGD is al geconfigureerd", + "no_devices_discovered": "Geen UPnP / IGD's ontdekt", + "no_sensors_or_port_mapping": "Schakel ten minste sensoren of poorttoewijzing in" + }, "step": { - "init": { - "title": "UPnP/IGD" - }, "user": { - "title": "Extra configuratie options voor UPnP/IGD", - "data":{ - "igd": "UPnP/IGD", - "enable_sensors": "Verkeer sensors toevoegen", - "enable_port_mapping": "Maak port mapping voor Home Assistant" - } + "data": { + "enable_port_mapping": "Poorttoewijzing voor Home Assistant inschakelen", + "enable_sensors": "Voeg verkeerssensoren toe" + }, + "title": "Configuratiemogelijkheden voor de UPnP/IGD" } }, - "error": { - }, - "abort": { - "no_devices_discovered": "Geen UPnP/IGDs gevonden", - "already_configured": "UPnP/IGD is reeds geconfigureerd", - "no_sensors_or_port_mapping": "Kies ten minste sensors of port mapping" - } + "title": "UPnP / IGD" } -} +} \ No newline at end of file diff --git a/homeassistant/components/upnp/.translations/pl.json b/homeassistant/components/upnp/.translations/pl.json new file mode 100644 index 00000000000..e47a25b9d93 --- /dev/null +++ b/homeassistant/components/upnp/.translations/pl.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "UPnP/IGD jest ju\u017c skonfigurowane", + "no_devices_discovered": "Nie wykryto urz\u0105dze\u0144 UPnP/IGD", + "no_sensors_or_port_mapping": "W\u0142\u0105cz przynajmniej sensory lub mapowanie port\u00f3w" + }, + "error": { + "few": "kilka", + "many": "wiele", + "one": "jeden", + "other": "inne" + }, + "step": { + "init": { + "title": "UPnP/IGD" + }, + "user": { + "data": { + "enable_port_mapping": "W\u0142\u0105cz mapowanie port\u00f3w dla Home Assistant'a", + "enable_sensors": "Dodaj sensor ruchu sieciowego", + "igd": "UPnP/IGD" + }, + "title": "Opcje konfiguracji dla UPnP/IGD" + } + }, + "title": "UPnP/IGD" + } +} \ No newline at end of file diff --git a/homeassistant/components/upnp/.translations/ru.json b/homeassistant/components/upnp/.translations/ru.json new file mode 100644 index 00000000000..9b7d358da0a --- /dev/null +++ b/homeassistant/components/upnp/.translations/ru.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "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.", + "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 " + }, + "step": { + "init": { + "title": "UPnP / IGD" + }, + "user": { + "data": { + "enable_port_mapping": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u043f\u0440\u0435\u043e\u0431\u0440\u0430\u0437\u043e\u0432\u0430\u043d\u0438\u0435 \u043f\u043e\u0440\u0442\u043e\u0432 \u0434\u043b\u044f Home Assistant", + "enable_sensors": "\u0414\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0434\u0430\u0442\u0447\u0438\u043a\u0438 \u0441\u0435\u0442\u0435\u0432\u043e\u0433\u043e \u0442\u0440\u0430\u0444\u0438\u043a\u0430", + "igd": "UPnP / IGD" + }, + "title": "\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0434\u043b\u044f UPnP / IGD" + } + }, + "title": "UPnP / IGD" + } +} \ No newline at end of file diff --git a/homeassistant/components/upnp/.translations/zh-Hans.json b/homeassistant/components/upnp/.translations/zh-Hans.json new file mode 100644 index 00000000000..c4962ba1c4b --- /dev/null +++ b/homeassistant/components/upnp/.translations/zh-Hans.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "UPnP/IGD \u5df2\u914d\u7f6e\u5b8c\u6210", + "no_devices_discovered": "\u672a\u53d1\u73b0 UPnP/IGD", + "no_sensors_or_port_mapping": "\u81f3\u5c11\u542f\u7528\u4f20\u611f\u5668\u6216\u7aef\u53e3\u6620\u5c04" + }, + "step": { + "init": { + "title": "UPnP/IGD" + }, + "user": { + "data": { + "enable_port_mapping": "\u4e3a Home Assistant \u542f\u7528\u7aef\u53e3\u6620\u5c04", + "enable_sensors": "\u6dfb\u52a0\u6d41\u91cf\u4f20\u611f\u5668", + "igd": "UPnP/IGD" + }, + "title": "UPnP/IGD \u7684\u914d\u7f6e\u9009\u9879" + } + }, + "title": "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 new file mode 100644 index 00000000000..ca8171265ae --- /dev/null +++ b/homeassistant/components/upnp/.translations/zh-Hant.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "UPnP/IGD \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "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" + }, + "step": { + "init": { + "title": "UPnP/IGD" + }, + "user": { + "data": { + "enable_port_mapping": "\u958b\u555f Home Assistant \u901a\u8a0a\u57e0\u8f49\u767c", + "enable_sensors": "\u65b0\u589e\u4ea4\u901a\u611f\u61c9\u5668", + "igd": "UPnP/IGD" + }, + "title": "UPnP/IGD \u8a2d\u5b9a\u9078\u9805" + } + }, + "title": "UPnP/IGD" + } +} \ No newline at end of file From 88c0a92857f652efd9823618527cc4cbc0d90afe Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 5 Oct 2018 17:56:30 +0200 Subject: [PATCH 210/247] Bumped version to 0.80.0b0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 93ab0deeba2..b10743244c4 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 80 -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 738089c57c001d552a69a7ccd11d356eaa6f2378 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 5 Oct 2018 20:21:09 +0200 Subject: [PATCH 211/247] Fix incorrect yaml in hangouts (#17169) --- homeassistant/components/hangouts/services.yaml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/hangouts/services.yaml b/homeassistant/components/hangouts/services.yaml index ded324d2de9..d07f1d65688 100644 --- a/homeassistant/components/hangouts/services.yaml +++ b/homeassistant/components/hangouts/services.yaml @@ -12,5 +12,4 @@ send_message: example: '[{"text":"test", "is_bold": false, "is_italic": false, "is_strikethrough": false, "is_underline": false, "parse_str": false, "link_target": "http://google.com"}, ...]' data: description: Other options ['image_file' / 'image_url'] - example: '{ "image_file": "file" }' or '{ "image_url": "url" }' - + example: '{ "image_file": "file" } or { "image_url": "url" }' From 7a22fbe96161c9aa0c8ea0d43d60805dd9b0ae38 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 5 Oct 2018 20:21:27 +0200 Subject: [PATCH 212/247] Bumped version to 0.80.0b1 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index b10743244c4..db1de6bbe6b 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 80 -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 31b1b0c0449508d5ebd1271df43653c7d34b88ab Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 7 Oct 2018 23:26:23 +0200 Subject: [PATCH 213/247] Update frontend to 20181007.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 9a5e7c05a56..27904faec1a 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==20181005.0'] +REQUIREMENTS = ['home-assistant-frontend==20181007.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log', diff --git a/requirements_all.txt b/requirements_all.txt index faadd0f5016..b4277836b47 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -458,7 +458,7 @@ hole==0.3.0 holidays==0.9.7 # homeassistant.components.frontend -home-assistant-frontend==20181005.0 +home-assistant-frontend==20181007.0 # homeassistant.components.homekit_controller # homekit==0.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 55b8733fff1..39eed8510e6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -90,7 +90,7 @@ hdate==0.6.3 holidays==0.9.7 # homeassistant.components.frontend -home-assistant-frontend==20181005.0 +home-assistant-frontend==20181007.0 # homeassistant.components.homematicip_cloud homematicip==0.9.8 From 4cffef7f2bd9760c109572ae3c5dcc64b553aa0b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 7 Oct 2018 23:26:46 +0200 Subject: [PATCH 214/247] Update translations --- .../components/hangouts/.translations/no.json | 2 ++ .../components/hue/.translations/no.json | 2 +- .../components/mqtt/.translations/no.json | 7 +++++ .../components/upnp/.translations/ko.json | 2 +- .../components/upnp/.translations/no.json | 29 +++++++++++++++++++ 5 files changed, 40 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/upnp/.translations/no.json diff --git a/homeassistant/components/hangouts/.translations/no.json b/homeassistant/components/hangouts/.translations/no.json index c2cdb93c005..d75092da759 100644 --- a/homeassistant/components/hangouts/.translations/no.json +++ b/homeassistant/components/hangouts/.translations/no.json @@ -14,6 +14,7 @@ "data": { "2fa": "2FA Pin" }, + "description": "Tom", "title": "Tofaktorautentisering" }, "user": { @@ -21,6 +22,7 @@ "email": "E-postadresse", "password": "Passord" }, + "description": "Tom", "title": "Google Hangouts p\u00e5logging" } }, diff --git a/homeassistant/components/hue/.translations/no.json b/homeassistant/components/hue/.translations/no.json index 309e9f6a299..02dd6ef7128 100644 --- a/homeassistant/components/hue/.translations/no.json +++ b/homeassistant/components/hue/.translations/no.json @@ -24,6 +24,6 @@ "title": "Link Hub" } }, - "title": "Philips Hue Bridge" + "title": "Philips Hue" } } \ No newline at end of file diff --git a/homeassistant/components/mqtt/.translations/no.json b/homeassistant/components/mqtt/.translations/no.json index 412efd3e107..b3f1e4740b9 100644 --- a/homeassistant/components/mqtt/.translations/no.json +++ b/homeassistant/components/mqtt/.translations/no.json @@ -17,6 +17,13 @@ }, "description": "Vennligst oppgi tilkoblingsinformasjonen for din MQTT megler.", "title": "MQTT" + }, + "hassio_confirm": { + "data": { + "discovery": "Aktiver oppdagelse" + }, + "description": "Vil du konfigurere Home Assistant til \u00e5 koble til MQTT megler gitt av hass.io tillegget {addon}?", + "title": "MQTT megler via Hass.io tillegg" } }, "title": "MQTT" diff --git a/homeassistant/components/upnp/.translations/ko.json b/homeassistant/components/upnp/.translations/ko.json index 4da5bedb9b3..0dd7a16de0b 100644 --- a/homeassistant/components/upnp/.translations/ko.json +++ b/homeassistant/components/upnp/.translations/ko.json @@ -6,7 +6,7 @@ "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" }, "error": { - "other": "\uc5d0\ub7ec" + "other": "\ub2e4\ub978" }, "step": { "init": { diff --git a/homeassistant/components/upnp/.translations/no.json b/homeassistant/components/upnp/.translations/no.json new file mode 100644 index 00000000000..fbb1b4afc75 --- /dev/null +++ b/homeassistant/components/upnp/.translations/no.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "UPnP / IGD er allerede konfigurert", + "no_devices_discovered": "Ingen UPnP / IGDs oppdaget" + }, + "error": { + "few": "f\u00e5", + "many": "mange", + "one": "en", + "other": "andre", + "two": "to", + "zero": "ingen" + }, + "step": { + "init": { + "title": "UPnP / IGD" + }, + "user": { + "data": { + "enable_sensors": "Legg til trafikk sensorer", + "igd": "UPnP / IGD" + }, + "title": "Konfigurasjonsalternativer for UPnP / IGD" + } + }, + "title": "UPnP / IGD" + } +} \ No newline at end of file From c7220919674f2f79f41ebdfb9a272ebdd9507cbb Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 5 Oct 2018 23:07:27 +0200 Subject: [PATCH 215/247] Fix data used for logbook (#17172) * Fix data used for logbook * Lint --- homeassistant/components/logbook.py | 7 ++++--- tests/components/test_logbook.py | 12 ++++++------ 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/logbook.py b/homeassistant/components/logbook.py index e282a133f1d..9e66c8d3aca 100644 --- a/homeassistant/components/logbook.py +++ b/homeassistant/components/logbook.py @@ -265,16 +265,17 @@ def humanify(hass, events): elif event.event_type == EVENT_ALEXA_SMART_HOME: data = event.data - entity_id = data.get('entity_id') + entity_id = data['request'].get('entity_id') if entity_id: state = hass.states.get(entity_id) name = state.name if state else entity_id message = "send command {}/{} for {}".format( - data['namespace'], data['name'], name) + data['request']['namespace'], + data['request']['name'], name) else: message = "send command {}/{}".format( - data['namespace'], data['name']) + data['request']['namespace'], data['request']['name']) yield { 'when': event.time_fired, diff --git a/tests/components/test_logbook.py b/tests/components/test_logbook.py index 9ccb8f58a87..8e7c2299731 100644 --- a/tests/components/test_logbook.py +++ b/tests/components/test_logbook.py @@ -565,20 +565,20 @@ async def test_humanify_alexa_event(hass): }) results = list(logbook.humanify(hass, [ - ha.Event(EVENT_ALEXA_SMART_HOME, { + ha.Event(EVENT_ALEXA_SMART_HOME, {'request': { 'namespace': 'Alexa.Discovery', 'name': 'Discover', - }), - ha.Event(EVENT_ALEXA_SMART_HOME, { + }}), + ha.Event(EVENT_ALEXA_SMART_HOME, {'request': { 'namespace': 'Alexa.PowerController', 'name': 'TurnOn', 'entity_id': 'light.kitchen' - }), - ha.Event(EVENT_ALEXA_SMART_HOME, { + }}), + ha.Event(EVENT_ALEXA_SMART_HOME, {'request': { 'namespace': 'Alexa.PowerController', 'name': 'TurnOn', 'entity_id': 'light.non_existing' - }), + }}), ])) From 7b176aa7c9a33000b87116dad717482bf2c0a97d Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Fri, 5 Oct 2018 23:09:55 +0200 Subject: [PATCH 216/247] Fix device_tracker service call & cleanup (#17173) * Bugfix group service - device_tracker * Cleanup --- homeassistant/components/alert.py | 6 ++-- .../components/device_sun_light_trigger.py | 20 ++++++----- .../components/device_tracker/__init__.py | 30 ++++++++-------- homeassistant/components/notify/telegram.py | 2 +- homeassistant/components/switch/flux.py | 36 ++++++++++--------- 5 files changed, 51 insertions(+), 43 deletions(-) diff --git a/homeassistant/components/alert.py b/homeassistant/components/alert.py index 72689b30138..e224351f9db 100644 --- a/homeassistant/components/alert.py +++ b/homeassistant/components/alert.py @@ -10,6 +10,8 @@ import logging import voluptuous as vol +from homeassistant.components.notify import ( + ATTR_MESSAGE, DOMAIN as DOMAIN_NOTIFY) from homeassistant.const import ( CONF_ENTITY_ID, STATE_IDLE, CONF_NAME, CONF_STATE, STATE_ON, STATE_OFF, SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE, ATTR_ENTITY_ID) @@ -204,7 +206,7 @@ class Alert(ToggleEntity): self._send_done_message = True for target in self._notifiers: await self.hass.services.async_call( - 'notify', target, {'message': self._name}) + DOMAIN_NOTIFY, target, {ATTR_MESSAGE: self._name}) await self._schedule_notify() async def _notify_done_message(self, *args): @@ -213,7 +215,7 @@ class Alert(ToggleEntity): self._send_done_message = False for target in self._notifiers: await self.hass.services.async_call( - 'notify', target, {'message': self._done_message}) + DOMAIN_NOTIFY, target, {ATTR_MESSAGE: self._done_message}) async def async_turn_on(self, **kwargs): """Async Unacknowledge alert.""" diff --git a/homeassistant/components/device_sun_light_trigger.py b/homeassistant/components/device_sun_light_trigger.py index cd81b3a01ad..40a602056bf 100644 --- a/homeassistant/components/device_sun_light_trigger.py +++ b/homeassistant/components/device_sun_light_trigger.py @@ -11,9 +11,11 @@ import voluptuous as vol from homeassistant.core import callback import homeassistant.util.dt as dt_util +from homeassistant.components.light import ( + ATTR_PROFILE, ATTR_TRANSITION, DOMAIN as DOMAIN_LIGHT) from homeassistant.const import ( - SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_HOME, STATE_NOT_HOME) -from homeassistant.components.light import DOMAIN as DOMAIN_LIGHT + ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_HOME, + STATE_NOT_HOME) from homeassistant.helpers.event import ( async_track_point_in_utc_time, async_track_state_change) from homeassistant.helpers.sun import is_up, get_astral_event_next @@ -88,10 +90,10 @@ async def async_setup(hass, config): return hass.async_create_task( hass.services.async_call( - DOMAIN_LIGHT, SERVICE_TURN_ON, dict( - entity_id=light_id, - transition=LIGHT_TRANSITION_TIME.seconds, - profile=light_profile))) + DOMAIN_LIGHT, SERVICE_TURN_ON, { + ATTR_ENTITY_ID: light_id, + ATTR_TRANSITION: LIGHT_TRANSITION_TIME.seconds, + ATTR_PROFILE: light_profile})) def async_turn_on_factory(light_id): """Generate turn on callbacks as factory.""" @@ -144,7 +146,7 @@ async def async_setup(hass, config): hass.async_create_task( hass.services.async_call( DOMAIN_LIGHT, SERVICE_TURN_ON, - dict(entity_id=light_ids, profile=light_profile))) + {ATTR_ENTITY_ID: light_ids, ATTR_PROFILE: light_profile})) # Are we in the time span were we would turn on the lights # if someone would be home? @@ -160,7 +162,7 @@ async def async_setup(hass, config): hass.async_create_task( hass.services.async_call( DOMAIN_LIGHT, SERVICE_TURN_ON, - dict(entity_id=light_id))) + {ATTR_ENTITY_ID: light_id})) else: # If this light didn't happen to be turned on yet so @@ -184,7 +186,7 @@ async def async_setup(hass, config): "Everyone has left but there are lights on. Turning them off") hass.async_create_task( hass.services.async_call( - DOMAIN_LIGHT, SERVICE_TURN_OFF, dict(entity_id=light_ids))) + DOMAIN_LIGHT, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: light_ids})) async_track_state_change( hass, device_group, turn_off_lights_when_all_leave, diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index a9cc122a6f9..cbf32b4cd5a 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -15,7 +15,9 @@ from homeassistant.setup import async_prepare_setup_platform from homeassistant.core import callback from homeassistant.loader import bind_hass from homeassistant.components import group, zone -from homeassistant.components.group import DOMAIN as DOMAIN_GROUP, SERVICE_SET +from homeassistant.components.group import ( + ATTR_ADD_ENTITIES, ATTR_ENTITIES, ATTR_OBJECT_ID, ATTR_VISIBLE, + DOMAIN as DOMAIN_GROUP, SERVICE_SET) from homeassistant.components.zone.zone import async_active_zone from homeassistant.config import load_yaml_config_file, async_log_exception from homeassistant.exceptions import HomeAssistantError @@ -32,9 +34,9 @@ from homeassistant.util.yaml import dump from homeassistant.helpers.event import async_track_utc_time_change from homeassistant.const import ( - ATTR_GPS_ACCURACY, ATTR_LATITUDE, ATTR_LONGITUDE, CONF_NAME, CONF_MAC, - DEVICE_DEFAULT_NAME, STATE_HOME, STATE_NOT_HOME, ATTR_ENTITY_ID, - CONF_ICON, ATTR_ICON, ATTR_NAME) + ATTR_ENTITY_ID, ATTR_GPS_ACCURACY, ATTR_ICON, ATTR_LATITUDE, + ATTR_LONGITUDE, ATTR_NAME, CONF_ICON, CONF_MAC, CONF_NAME, + DEVICE_DEFAULT_NAME, STATE_NOT_HOME, STATE_HOME) _LOGGER = logging.getLogger(__name__) @@ -317,11 +319,11 @@ class DeviceTracker: if self.group and self.track_new: self.hass.async_create_task( self.hass.async_call( - DOMAIN_GROUP, SERVICE_SET, dict( - object_id=util.slugify(GROUP_NAME_ALL_DEVICES), - visible=False, - name=GROUP_NAME_ALL_DEVICES, - add=[device.entity_id]))) + DOMAIN_GROUP, SERVICE_SET, { + ATTR_OBJECT_ID: util.slugify(GROUP_NAME_ALL_DEVICES), + ATTR_VISIBLE: False, + ATTR_NAME: GROUP_NAME_ALL_DEVICES, + ATTR_ADD_ENTITIES: [device.entity_id]})) self.hass.bus.async_fire(EVENT_NEW_DEVICE, { ATTR_ENTITY_ID: device.entity_id, @@ -356,11 +358,11 @@ class DeviceTracker: self.hass.async_create_task( self.hass.services.async_call( - DOMAIN_GROUP, SERVICE_SET, dict( - object_id=util.slugify(GROUP_NAME_ALL_DEVICES), - visible=False, - name=GROUP_NAME_ALL_DEVICES, - entities=entity_ids))) + DOMAIN_GROUP, SERVICE_SET, { + ATTR_OBJECT_ID: util.slugify(GROUP_NAME_ALL_DEVICES), + ATTR_VISIBLE: False, + ATTR_NAME: GROUP_NAME_ALL_DEVICES, + ATTR_ENTITIES: entity_ids})) @callback def async_update_stale(self, now: dt_util.dt.datetime): diff --git a/homeassistant/components/notify/telegram.py b/homeassistant/components/notify/telegram.py index b012506acd9..1dff82fa2cd 100644 --- a/homeassistant/components/notify/telegram.py +++ b/homeassistant/components/notify/telegram.py @@ -47,7 +47,7 @@ class TelegramNotificationService(BaseNotificationService): def send_message(self, message="", **kwargs): """Send a message to a user.""" - service_data = dict(target=kwargs.get(ATTR_TARGET, self._chat_id)) + service_data = {ATTR_TARGET: kwargs.get(ATTR_TARGET, self._chat_id)} if ATTR_TITLE in kwargs: service_data.update({ATTR_TITLE: kwargs.get(ATTR_TITLE)}) if message: diff --git a/homeassistant/components/switch/flux.py b/homeassistant/components/switch/flux.py index 793d6ab91d0..c541c37b5e7 100644 --- a/homeassistant/components/switch/flux.py +++ b/homeassistant/components/switch/flux.py @@ -13,10 +13,12 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.components.light import ( - is_on, DOMAIN as LIGHT_DOMAIN, VALID_TRANSITION, ATTR_TRANSITION) + is_on, ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_RGB_COLOR, ATTR_TRANSITION, + ATTR_WHITE_VALUE, ATTR_XY_COLOR, DOMAIN as LIGHT_DOMAIN, VALID_TRANSITION) from homeassistant.components.switch import DOMAIN, SwitchDevice from homeassistant.const import ( - CONF_NAME, CONF_PLATFORM, CONF_LIGHTS, CONF_MODE, SERVICE_TURN_ON) + ATTR_ENTITY_ID, CONF_NAME, CONF_PLATFORM, CONF_LIGHTS, CONF_MODE, + SERVICE_TURN_ON) from homeassistant.helpers.event import track_time_change from homeassistant.helpers.sun import get_astral_event_date from homeassistant.util import slugify @@ -70,12 +72,12 @@ def set_lights_xy(hass, lights, x_val, y_val, brightness, transition): for light in lights: if is_on(hass, light): hass.services.call( - LIGHT_DOMAIN, SERVICE_TURN_ON, dict( - xy_color=[x_val, y_val], - brightness=brightness, - transition=transition, - white_value=brightness, - entity_id=light)) + LIGHT_DOMAIN, SERVICE_TURN_ON, { + ATTR_XY_COLOR: [x_val, y_val], + ATTR_BRIGHTNESS: brightness, + ATTR_TRANSITION: transition, + ATTR_WHITE_VALUE: brightness, + ATTR_ENTITY_ID: light}) def set_lights_temp(hass, lights, mired, brightness, transition): @@ -83,11 +85,11 @@ def set_lights_temp(hass, lights, mired, brightness, transition): for light in lights: if is_on(hass, light): hass.services.call( - LIGHT_DOMAIN, SERVICE_TURN_ON, dict( - color_temp=int(mired), - brightness=brightness, - transition=transition, - entity_id=light)) + LIGHT_DOMAIN, SERVICE_TURN_ON, { + ATTR_COLOR_TEMP: int(mired), + ATTR_BRIGHTNESS: brightness, + ATTR_TRANSITION: transition, + ATTR_ENTITY_ID: light}) def set_lights_rgb(hass, lights, rgb, transition): @@ -95,10 +97,10 @@ def set_lights_rgb(hass, lights, rgb, transition): for light in lights: if is_on(hass, light): hass.services.call( - LIGHT_DOMAIN, SERVICE_TURN_ON, dict( - rgb_color=rgb, - transition=transition, - entity_id=light)) + LIGHT_DOMAIN, SERVICE_TURN_ON, { + ATTR_RGB_COLOR: rgb, + ATTR_TRANSITION: transition, + ATTR_ENTITY_ID: light}) def setup_platform(hass, config, add_entities, discovery_info=None): From 3338ebd4c161f3e02269b94cf79d2b53bc70d6f9 Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Sat, 6 Oct 2018 23:30:07 +0200 Subject: [PATCH 217/247] Bugfix switch flux - light service call (#17187) * Bugfix switch flux - light service call * Change x_val and y_val test --- homeassistant/components/switch/flux.py | 38 +++++++++++++++---------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/switch/flux.py b/homeassistant/components/switch/flux.py index c541c37b5e7..05e0497155a 100644 --- a/homeassistant/components/switch/flux.py +++ b/homeassistant/components/switch/flux.py @@ -71,36 +71,44 @@ def set_lights_xy(hass, lights, x_val, y_val, brightness, transition): """Set color of array of lights.""" for light in lights: if is_on(hass, light): + service_data = {ATTR_ENTITY_ID: light} + if x_val is not None and y_val is not None: + service_data[ATTR_XY_COLOR] = [x_val, y_val] + if brightness is not None: + service_data[ATTR_BRIGHTNESS] = brightness + service_data[ATTR_WHITE_VALUE] = brightness + if transition is not None: + service_data[ATTR_TRANSITION] = transition hass.services.call( - LIGHT_DOMAIN, SERVICE_TURN_ON, { - ATTR_XY_COLOR: [x_val, y_val], - ATTR_BRIGHTNESS: brightness, - ATTR_TRANSITION: transition, - ATTR_WHITE_VALUE: brightness, - ATTR_ENTITY_ID: light}) + LIGHT_DOMAIN, SERVICE_TURN_ON, service_data) def set_lights_temp(hass, lights, mired, brightness, transition): """Set color of array of lights.""" for light in lights: if is_on(hass, light): + service_data = {ATTR_ENTITY_ID: light} + if mired is not None: + service_data[ATTR_COLOR_TEMP] = int(mired) + if brightness is not None: + service_data[ATTR_BRIGHTNESS] = brightness + if transition is not None: + service_data[ATTR_TRANSITION] = transition hass.services.call( - LIGHT_DOMAIN, SERVICE_TURN_ON, { - ATTR_COLOR_TEMP: int(mired), - ATTR_BRIGHTNESS: brightness, - ATTR_TRANSITION: transition, - ATTR_ENTITY_ID: light}) + LIGHT_DOMAIN, SERVICE_TURN_ON, service_data) def set_lights_rgb(hass, lights, rgb, transition): """Set color of array of lights.""" for light in lights: if is_on(hass, light): + service_data = {ATTR_ENTITY_ID: light} + if rgb is not None: + service_data[ATTR_RGB_COLOR] = rgb + if transition is not None: + service_data[ATTR_TRANSITION] = transition hass.services.call( - LIGHT_DOMAIN, SERVICE_TURN_ON, { - ATTR_RGB_COLOR: rgb, - ATTR_TRANSITION: transition, - ATTR_ENTITY_ID: light}) + LIGHT_DOMAIN, SERVICE_TURN_ON, service_data) def setup_platform(hass, config, add_entities, discovery_info=None): From b872cb95c796feff25feef80e99f268c341aa81e Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Sat, 6 Oct 2018 14:32:54 +0200 Subject: [PATCH 218/247] Upgrade aiolifx_effects to 0.2.1 (#17188) --- homeassistant/components/light/lifx.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/light/lifx.py b/homeassistant/components/light/lifx.py index 17b9f104f68..9dcd2ae4cc2 100644 --- a/homeassistant/components/light/lifx.py +++ b/homeassistant/components/light/lifx.py @@ -30,7 +30,7 @@ import homeassistant.util.color as color_util _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['aiolifx==0.6.3', 'aiolifx_effects==0.2.0'] +REQUIREMENTS = ['aiolifx==0.6.3', 'aiolifx_effects==0.2.1'] UDP_BROADCAST_PORT = 56700 diff --git a/requirements_all.txt b/requirements_all.txt index b4277836b47..8ed5109afeb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -110,7 +110,7 @@ aioimaplib==0.7.13 aiolifx==0.6.3 # homeassistant.components.light.lifx -aiolifx_effects==0.2.0 +aiolifx_effects==0.2.1 # homeassistant.components.scene.hunterdouglas_powerview aiopvapi==1.5.4 From 7369af06391aca0e2aad28fdced3a00ea0a9dbd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Per=20Sandstr=C3=B6m?= Date: Sat, 6 Oct 2018 20:03:22 +0200 Subject: [PATCH 219/247] Verisure standard config for scan interval (#17192) * verisure configurable polling * fix indentation --- homeassistant/components/verisure.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/verisure.py b/homeassistant/components/verisure.py index 3876ff41c37..016547697b9 100644 --- a/homeassistant/components/verisure.py +++ b/homeassistant/components/verisure.py @@ -10,8 +10,8 @@ from datetime import timedelta import voluptuous as vol -from homeassistant.const import (CONF_PASSWORD, CONF_USERNAME, - EVENT_HOMEASSISTANT_STOP) +from homeassistant.const import (CONF_PASSWORD, CONF_SCAN_INTERVAL, + CONF_USERNAME, EVENT_HOMEASSISTANT_STOP) from homeassistant.helpers import discovery from homeassistant.util import Throttle import homeassistant.helpers.config_validation as cv @@ -32,10 +32,12 @@ CONF_MOUSE = 'mouse' CONF_SMARTPLUGS = 'smartplugs' CONF_THERMOMETERS = 'thermometers' CONF_SMARTCAM = 'smartcam' -CONF_POLLING_RATE = 'polling_rate' DOMAIN = 'verisure' +MIN_SCAN_INTERVAL = timedelta(minutes=1) +DEFAULT_SCAN_INTERVAL = timedelta(minutes=1) + SERVICE_CAPTURE_SMARTCAM = 'capture_smartcam' HUB = None @@ -54,8 +56,8 @@ CONFIG_SCHEMA = vol.Schema({ vol.Optional(CONF_SMARTPLUGS, default=True): cv.boolean, vol.Optional(CONF_THERMOMETERS, default=True): cv.boolean, vol.Optional(CONF_SMARTCAM, default=True): cv.boolean, - vol.Optional(CONF_POLLING_RATE, default=1): vol.All( - vol.Coerce(int), vol.Range(min=1)), + vol.Optional(CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL): ( + vol.All(cv.time_period, vol.Clamp(min=MIN_SCAN_INTERVAL))), }), }, extra=vol.ALLOW_EXTRA) @@ -69,8 +71,8 @@ def setup(hass, config): import verisure global HUB HUB = VerisureHub(config[DOMAIN], verisure) - HUB.update_overview = Throttle(timedelta( - minutes=config[DOMAIN][CONF_POLLING_RATE]))(HUB.update_overview) + HUB.update_overview = Throttle( + config[DOMAIN][CONF_SCAN_INTERVAL])(HUB.update_overview) if not HUB.login(): return False hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, From 5fcea074c4c938e96f80fa6f6bac5836b5368ce6 Mon Sep 17 00:00:00 2001 From: Martin Berg <2682426+mbrrg@users.noreply.github.com> Date: Sun, 7 Oct 2018 23:30:09 +0200 Subject: [PATCH 220/247] Init sub-components using global var. (#17220) --- .../components/alarm_control_panel/spc.py | 12 ++-- homeassistant/components/binary_sensor/spc.py | 14 ++--- homeassistant/components/spc.py | 9 +-- .../alarm_control_panel/test_spc.py | 58 ------------------- tests/components/binary_sensor/test_spc.py | 55 ------------------ tests/components/test_spc.py | 3 +- 6 files changed, 13 insertions(+), 138 deletions(-) delete mode 100644 tests/components/alarm_control_panel/test_spc.py delete mode 100644 tests/components/binary_sensor/test_spc.py diff --git a/homeassistant/components/alarm_control_panel/spc.py b/homeassistant/components/alarm_control_panel/spc.py index 9150518022f..2345717d835 100644 --- a/homeassistant/components/alarm_control_panel/spc.py +++ b/homeassistant/components/alarm_control_panel/spc.py @@ -9,8 +9,7 @@ import logging import homeassistant.components.alarm_control_panel as alarm from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.core import callback -from homeassistant.components.spc import ( - ATTR_DISCOVER_AREAS, DATA_API, SIGNAL_UPDATE_ALARM) +from homeassistant.components.spc import (DATA_API, SIGNAL_UPDATE_ALARM) from homeassistant.const import ( STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED) @@ -37,12 +36,9 @@ def _get_alarm_state(area): async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the SPC alarm control panel platform.""" - if (discovery_info is None or - discovery_info[ATTR_DISCOVER_AREAS] is None): - return - - async_add_entities([SpcAlarm(area=area, api=hass.data[DATA_API]) - for area in discovery_info[ATTR_DISCOVER_AREAS]]) + api = hass.data[DATA_API] + async_add_entities([SpcAlarm(area=area, api=api) + for area in api.areas.values()]) class SpcAlarm(alarm.AlarmControlPanel): diff --git a/homeassistant/components/binary_sensor/spc.py b/homeassistant/components/binary_sensor/spc.py index c1be72db374..25baf503399 100644 --- a/homeassistant/components/binary_sensor/spc.py +++ b/homeassistant/components/binary_sensor/spc.py @@ -9,8 +9,7 @@ import logging from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.core import callback -from homeassistant.components.spc import ( - ATTR_DISCOVER_DEVICES, SIGNAL_UPDATE_SENSOR) +from homeassistant.components.spc import (DATA_API, SIGNAL_UPDATE_SENSOR) _LOGGER = logging.getLogger(__name__) @@ -27,13 +26,10 @@ def _get_device_class(zone_type): async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the SPC binary sensor.""" - if (discovery_info is None or - discovery_info[ATTR_DISCOVER_DEVICES] is None): - return - - async_add_entities(SpcBinarySensor(zone) - for zone in discovery_info[ATTR_DISCOVER_DEVICES] - if _get_device_class(zone.type)) + api = hass.data[DATA_API] + async_add_entities([SpcBinarySensor(zone) + for zone in api.zones.values() + if _get_device_class(zone.type)]) class SpcBinarySensor(BinarySensorDevice): diff --git a/homeassistant/components/spc.py b/homeassistant/components/spc.py index b00a4aeed2c..0771608f88e 100644 --- a/homeassistant/components/spc.py +++ b/homeassistant/components/spc.py @@ -16,9 +16,6 @@ REQUIREMENTS = ['pyspcwebgw==0.4.0'] _LOGGER = logging.getLogger(__name__) -ATTR_DISCOVER_DEVICES = 'devices' -ATTR_DISCOVER_AREAS = 'areas' - CONF_WS_URL = 'ws_url' CONF_API_URL = 'api_url' @@ -66,13 +63,11 @@ async def async_setup(hass, config): # add sensor devices for each zone (typically motion/fire/door sensors) hass.async_create_task(discovery.async_load_platform( - hass, 'binary_sensor', DOMAIN, - {ATTR_DISCOVER_DEVICES: spc.zones.values()}, config)) + hass, 'binary_sensor', DOMAIN)) # create a separate alarm panel for each area hass.async_create_task(discovery.async_load_platform( - hass, 'alarm_control_panel', DOMAIN, - {ATTR_DISCOVER_AREAS: spc.areas.values()}, config)) + hass, 'alarm_control_panel', DOMAIN)) # start listening for incoming events over websocket spc.start() diff --git a/tests/components/alarm_control_panel/test_spc.py b/tests/components/alarm_control_panel/test_spc.py deleted file mode 100644 index b1078e1b14f..00000000000 --- a/tests/components/alarm_control_panel/test_spc.py +++ /dev/null @@ -1,58 +0,0 @@ -"""Tests for Vanderbilt SPC alarm control panel platform.""" -from homeassistant.components.alarm_control_panel import spc -from homeassistant.const import ( - STATE_ALARM_ARMED_AWAY, STATE_ALARM_DISARMED) -from homeassistant.components.spc import (DATA_API) - - -async def test_setup_platform(hass): - """Test adding areas as separate alarm control panel devices.""" - added_entities = [] - - def add_entities(entities): - nonlocal added_entities - added_entities = list(entities) - - area_defs = [{ - 'id': '1', - 'name': 'House', - 'mode': '3', - 'last_set_time': '1485759851', - 'last_set_user_id': '1', - 'last_set_user_name': 'Pelle', - 'last_unset_time': '1485800564', - 'last_unset_user_id': '1', - 'last_unset_user_name': 'Lisa', - 'last_alarm': '1478174896' - }, { - 'id': '3', - 'name': 'Garage', - 'mode': '0', - 'last_set_time': '1483705803', - 'last_set_user_id': '9998', - 'last_set_user_name': 'Pelle', - 'last_unset_time': '1483705808', - 'last_unset_user_id': '9998', - 'last_unset_user_name': 'Lisa' - }] - - from pyspcwebgw import Area - - areas = [Area(gateway=None, spc_area=a) for a in area_defs] - - hass.data[DATA_API] = None - - await spc.async_setup_platform(hass=hass, - config={}, - async_add_entities=add_entities, - discovery_info={'areas': areas}) - - assert len(added_entities) == 2 - - assert added_entities[0].name == 'House' - assert added_entities[0].state == STATE_ALARM_ARMED_AWAY - assert added_entities[0].changed_by == 'Pelle' - - assert added_entities[1].name == 'Garage' - assert added_entities[1].state == STATE_ALARM_DISARMED - assert added_entities[1].changed_by == 'Lisa' diff --git a/tests/components/binary_sensor/test_spc.py b/tests/components/binary_sensor/test_spc.py deleted file mode 100644 index ec0886aeed8..00000000000 --- a/tests/components/binary_sensor/test_spc.py +++ /dev/null @@ -1,55 +0,0 @@ -"""Tests for Vanderbilt SPC binary sensor platform.""" -from homeassistant.components.binary_sensor import spc - - -async def test_setup_platform(hass): - """Test autodiscovery of supported device types.""" - added_entities = [] - - zone_defs = [{ - 'id': '1', - 'type': '3', - 'zone_name': 'Kitchen smoke', - 'area': '1', - 'area_name': 'House', - 'input': '0', - 'status': '0', - }, { - 'id': '3', - 'type': '0', - 'zone_name': 'Hallway PIR', - 'area': '1', - 'area_name': 'House', - 'input': '0', - 'status': '0', - }, { - 'id': '5', - 'type': '1', - 'zone_name': 'Front door', - 'area': '1', - 'area_name': 'House', - 'input': '1', - 'status': '0', - }] - - def add_entities(entities): - nonlocal added_entities - added_entities = list(entities) - - from pyspcwebgw import Zone - - zones = [Zone(area=None, spc_zone=z) for z in zone_defs] - - await spc.async_setup_platform(hass=hass, - config={}, - async_add_entities=add_entities, - discovery_info={'devices': zones}) - - assert len(added_entities) == 3 - assert added_entities[0].device_class == 'smoke' - assert added_entities[0].state == 'off' - assert added_entities[1].device_class == 'motion' - assert added_entities[1].state == 'off' - assert added_entities[2].device_class == 'opening' - assert added_entities[2].state == 'on' - assert all(d.hidden for d in added_entities) diff --git a/tests/components/test_spc.py b/tests/components/test_spc.py index d4bedda4e96..bcbf970a48b 100644 --- a/tests/components/test_spc.py +++ b/tests/components/test_spc.py @@ -59,7 +59,8 @@ async def test_update_alarm_device(hass): return_value=mock_coro(True)): assert await async_setup_component(hass, 'spc', config) is True - await hass.async_block_till_done() + await hass.async_block_till_done() + entity_id = 'alarm_control_panel.house' assert hass.states.get(entity_id).state == STATE_ALARM_ARMED_AWAY From bd4ff6fc21850329cfe903dfe2508f9908e61086 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 7 Oct 2018 23:33:46 +0200 Subject: [PATCH 221/247] Bumped version to 0.80.0b2 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index db1de6bbe6b..e8e7ef95db2 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 80 -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 6be52208fc6b803e85aca53d0f99f948ea074bd6 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 8 Oct 2018 09:30:40 +0200 Subject: [PATCH 222/247] Prevent accidental device reg override (#17136) --- homeassistant/helpers/entity_platform.py | 27 +++++++++------ tests/helpers/test_entity_platform.py | 42 ++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 10 deletions(-) diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index f2913e37339..99aa10013ab 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -275,17 +275,24 @@ class EntityPlatform: device_info = entity.device_info if config_entry_id is not None and device_info is not None: + processed_dev_info = { + 'config_entry_id': config_entry_id + } + for key in ( + 'connections', + 'identifiers', + 'manufacturer', + 'model', + 'name', + 'sw_version', + 'via_hub', + ): + if key in device_info: + processed_dev_info[key] = device_info[key] + device = device_registry.async_get_or_create( - config_entry_id=config_entry_id, - connections=device_info.get('connections') or set(), - identifiers=device_info.get('identifiers') or set(), - manufacturer=device_info.get('manufacturer'), - model=device_info.get('model'), - name=device_info.get('name'), - sw_version=device_info.get('sw_version'), - via_hub=device_info.get('via_hub')) - if device: - device_id = device.id + **processed_dev_info) + device_id = device.id else: device_id = None diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index 631d446d186..97d6a0f5b98 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -728,3 +728,45 @@ async def test_device_info_called(hass): assert device.name == 'test-name' assert device.sw_version == 'test-sw' assert device.hub_device_id == hub.id + + +async def test_device_info_not_overrides(hass): + """Test device info is forwarded correctly.""" + registry = await hass.helpers.device_registry.async_get_registry() + device = registry.async_get_or_create( + config_entry_id='bla', + connections={('mac', 'abcd')}, + manufacturer='test-manufacturer', + model='test-model' + ) + + assert device.manufacturer == 'test-manufacturer' + assert device.model == 'test-model' + + async def async_setup_entry(hass, config_entry, async_add_entities): + """Mock setup entry method.""" + async_add_entities([ + MockEntity(unique_id='qwer', device_info={ + 'connections': {('mac', 'abcd')}, + }), + ]) + return True + + platform = MockPlatform( + async_setup_entry=async_setup_entry + ) + config_entry = MockConfigEntry(entry_id='super-mock-id') + entity_platform = MockEntityPlatform( + hass, + platform_name=config_entry.domain, + platform=platform + ) + + assert await entity_platform.async_setup_entry(config_entry) + await hass.async_block_till_done() + + device2 = registry.async_get_device(set(), {('mac', 'abcd')}) + assert device2 is not None + assert device.id == device2.id + assert device2.manufacturer == 'test-manufacturer' + assert device2.model == 'test-model' From 9e386938bb1e4b8bc90ba32fb5ce7c345dc8182b Mon Sep 17 00:00:00 2001 From: Matt Schmitt Date: Mon, 8 Oct 2018 08:19:23 -0400 Subject: [PATCH 223/247] MyQ cover return unknown state if not available (#17207) * Add additional supported states * Use get method for lookup * Return None if unable to get status --- homeassistant/components/cover/myq.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/cover/myq.py b/homeassistant/components/cover/myq.py index 78b6f891f11..5ceb4260d0c 100644 --- a/homeassistant/components/cover/myq.py +++ b/homeassistant/components/cover/myq.py @@ -11,8 +11,8 @@ import voluptuous as vol from homeassistant.components.cover import ( CoverDevice, SUPPORT_CLOSE, SUPPORT_OPEN) from homeassistant.const import ( - CONF_PASSWORD, CONF_TYPE, CONF_USERNAME, STATE_CLOSED, STATE_OPEN, - STATE_CLOSING, STATE_OPENING) + CONF_PASSWORD, CONF_TYPE, CONF_USERNAME, STATE_CLOSED, STATE_CLOSING, + STATE_OPEN, STATE_OPENING) import homeassistant.helpers.config_validation as cv REQUIREMENTS = ['pymyq==0.0.15'] @@ -23,8 +23,8 @@ DEFAULT_NAME = 'myq' MYQ_TO_HASS = { 'closed': STATE_CLOSED, - 'open': STATE_OPEN, 'closing': STATE_CLOSING, + 'open': STATE_OPEN, 'opening': STATE_OPENING } @@ -76,7 +76,7 @@ class MyQDevice(CoverDevice): self.myq = myq self.device_id = device['deviceid'] self._name = device['name'] - self._status = STATE_CLOSED + self._status = None @property def device_class(self): @@ -96,17 +96,19 @@ class MyQDevice(CoverDevice): @property def is_closed(self): """Return true if cover is closed, else False.""" - return MYQ_TO_HASS[self._status] == STATE_CLOSED + if self._status in [None, False]: + return None + return MYQ_TO_HASS.get(self._status) == STATE_CLOSED @property def is_closing(self): """Return if the cover is closing or not.""" - return MYQ_TO_HASS[self._status] == STATE_CLOSING + return MYQ_TO_HASS.get(self._status) == STATE_CLOSING @property def is_opening(self): """Return if the cover is opening or not.""" - return MYQ_TO_HASS[self._status] == STATE_OPENING + return MYQ_TO_HASS.get(self._status) == STATE_OPENING def close_cover(self, **kwargs): """Issue close command to cover.""" From 6671cbb96dfb4ef41be910f12140b1c34e3b5989 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Mon, 8 Oct 2018 10:59:43 +0200 Subject: [PATCH 224/247] Fix potential MQTT discovery race condition (#17208) * Fix potential MQTT discovery race condition * Rename data key --- homeassistant/components/mqtt/discovery.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py index fdb7948e4bf..a762978a330 100644 --- a/homeassistant/components/mqtt/discovery.py +++ b/homeassistant/components/mqtt/discovery.py @@ -4,6 +4,7 @@ Support for MQTT discovery. For more details about this component, please refer to the documentation at https://home-assistant.io/components/mqtt/#discovery """ +import asyncio import json import logging import re @@ -51,6 +52,7 @@ CONFIG_ENTRY_PLATFORMS = { } ALREADY_DISCOVERED = 'mqtt_discovered_components' +DATA_CONFIG_ENTRY_LOCK = 'mqtt_config_entry_lock' CONFIG_ENTRY_IS_SETUP = 'mqtt_config_entry_is_setup' MQTT_DISCOVERY_UPDATED = 'mqtt_discovery_updated_{}' MQTT_DISCOVERY_NEW = 'mqtt_discovery_new_{}_{}' @@ -119,14 +121,16 @@ async def async_start(hass: HomeAssistantType, discovery_topic, hass_config, return config_entries_key = '{}.{}'.format(component, platform) - if config_entries_key not in hass.data[CONFIG_ENTRY_IS_SETUP]: - hass.data[CONFIG_ENTRY_IS_SETUP].add(config_entries_key) - await hass.config_entries.async_forward_entry_setup( - config_entry, component) + async with hass.data[DATA_CONFIG_ENTRY_LOCK]: + if config_entries_key not in hass.data[CONFIG_ENTRY_IS_SETUP]: + await hass.config_entries.async_forward_entry_setup( + config_entry, component) + hass.data[CONFIG_ENTRY_IS_SETUP].add(config_entries_key) async_dispatcher_send(hass, MQTT_DISCOVERY_NEW.format( component, platform), payload) + hass.data[DATA_CONFIG_ENTRY_LOCK] = asyncio.Lock() hass.data[CONFIG_ENTRY_IS_SETUP] = set() await mqtt.async_subscribe( From 3a6eac216c8d03215bb364e1c090779f095e0682 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 8 Oct 2018 10:20:18 +0200 Subject: [PATCH 225/247] Fix SPC (#17236) --- homeassistant/components/alarm_control_panel/spc.py | 2 ++ homeassistant/components/binary_sensor/spc.py | 2 ++ homeassistant/components/spc.py | 4 ++-- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/alarm_control_panel/spc.py b/homeassistant/components/alarm_control_panel/spc.py index 2345717d835..b4c49d4d190 100644 --- a/homeassistant/components/alarm_control_panel/spc.py +++ b/homeassistant/components/alarm_control_panel/spc.py @@ -36,6 +36,8 @@ def _get_alarm_state(area): async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the SPC alarm control panel platform.""" + if discovery_info is None: + return api = hass.data[DATA_API] async_add_entities([SpcAlarm(area=area, api=api) for area in api.areas.values()]) diff --git a/homeassistant/components/binary_sensor/spc.py b/homeassistant/components/binary_sensor/spc.py index 25baf503399..baa25266804 100644 --- a/homeassistant/components/binary_sensor/spc.py +++ b/homeassistant/components/binary_sensor/spc.py @@ -26,6 +26,8 @@ def _get_device_class(zone_type): async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the SPC binary sensor.""" + if discovery_info is None: + return api = hass.data[DATA_API] async_add_entities([SpcBinarySensor(zone) for zone in api.zones.values() diff --git a/homeassistant/components/spc.py b/homeassistant/components/spc.py index 0771608f88e..5aa987bd0a8 100644 --- a/homeassistant/components/spc.py +++ b/homeassistant/components/spc.py @@ -63,11 +63,11 @@ async def async_setup(hass, config): # add sensor devices for each zone (typically motion/fire/door sensors) hass.async_create_task(discovery.async_load_platform( - hass, 'binary_sensor', DOMAIN)) + hass, 'binary_sensor', DOMAIN, {})) # create a separate alarm panel for each area hass.async_create_task(discovery.async_load_platform( - hass, 'alarm_control_panel', DOMAIN)) + hass, 'alarm_control_panel', DOMAIN, {})) # start listening for incoming events over websocket spc.start() From 2e120061b4f4db90801cadbef61d18ab9429c951 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 8 Oct 2018 12:53:51 +0200 Subject: [PATCH 226/247] Guard for bad device info (#17238) --- homeassistant/helpers/entity_platform.py | 6 +++--- tests/helpers/test_entity_platform.py | 2 ++ 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 99aa10013ab..3ab45577236 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -273,6 +273,7 @@ class EntityPlatform: config_entry_id = None device_info = entity.device_info + device_id = None if config_entry_id is not None and device_info is not None: processed_dev_info = { @@ -292,9 +293,8 @@ class EntityPlatform: device = device_registry.async_get_or_create( **processed_dev_info) - device_id = device.id - else: - device_id = None + if device: + device_id = device.id entry = entity_registry.async_get_or_create( self.domain, self.platform_name, entity.unique_id, diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index 97d6a0f5b98..e985771e486 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -719,6 +719,8 @@ async def test_device_info_called(hass): assert await entity_platform.async_setup_entry(config_entry) await hass.async_block_till_done() + assert len(hass.states.async_entity_ids()) == 2 + device = registry.async_get_device({('hue', '1234')}, set()) assert device is not None assert device.identifiers == {('hue', '1234')} From a372053eac3fd27e4ba904d852058368bcbf0555 Mon Sep 17 00:00:00 2001 From: Malte Franken Date: Mon, 8 Oct 2018 22:32:16 +1100 Subject: [PATCH 227/247] updated georss-client library to 0.3 (#17239) --- homeassistant/components/sensor/geo_rss_events.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sensor/geo_rss_events.py b/homeassistant/components/sensor/geo_rss_events.py index 2a9041df357..5085e113e92 100644 --- a/homeassistant/components/sensor/geo_rss_events.py +++ b/homeassistant/components/sensor/geo_rss_events.py @@ -19,7 +19,7 @@ from homeassistant.const import ( STATE_UNKNOWN, CONF_UNIT_OF_MEASUREMENT, CONF_NAME, CONF_RADIUS, CONF_URL) from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['georss_client==0.1'] +REQUIREMENTS = ['georss_client==0.3'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 8ed5109afeb..7da7f7e7847 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -398,7 +398,7 @@ geizhals==0.0.7 geojson_client==0.1 # homeassistant.components.sensor.geo_rss_events -georss_client==0.1 +georss_client==0.3 # homeassistant.components.sensor.gitter gitterpy==0.1.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 39eed8510e6..b4e995d580c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -72,7 +72,7 @@ gTTS-token==1.1.2 geojson_client==0.1 # homeassistant.components.sensor.geo_rss_events -georss_client==0.1 +georss_client==0.3 # homeassistant.components.ffmpeg ha-ffmpeg==1.9 From 4d3d51635d2637b177c290ec47ab6fa97cabda5b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 8 Oct 2018 16:16:01 +0200 Subject: [PATCH 228/247] Bumped version to 0.80.0b3 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index e8e7ef95db2..628d418a735 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 80 -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 429f09deb3175b82a52790d732bd6a388533981c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 8 Oct 2018 20:16:37 +0200 Subject: [PATCH 229/247] Add a webhook automation trigger (#17246) --- .../components/automation/webhook.py | 54 +++++++++++++ tests/components/automation/test_webhook.py | 75 +++++++++++++++++++ 2 files changed, 129 insertions(+) create mode 100644 homeassistant/components/automation/webhook.py create mode 100644 tests/components/automation/test_webhook.py diff --git a/homeassistant/components/automation/webhook.py b/homeassistant/components/automation/webhook.py new file mode 100644 index 00000000000..2c9c331cdc5 --- /dev/null +++ b/homeassistant/components/automation/webhook.py @@ -0,0 +1,54 @@ +""" +Offer webhook triggered automation rules. + +For more details about this automation rule, please refer to the documentation +at https://home-assistant.io/docs/automation/trigger/#webhook-trigger +""" +from functools import partial +import logging + +from aiohttp import hdrs +import voluptuous as vol + +from homeassistant.core import callback +from homeassistant.const import CONF_PLATFORM +import homeassistant.helpers.config_validation as cv + +DEPENDENCIES = ('webhook',) + +_LOGGER = logging.getLogger(__name__) +CONF_WEBHOOK_ID = 'webhook_id' + +TRIGGER_SCHEMA = vol.Schema({ + vol.Required(CONF_PLATFORM): 'webhook', + vol.Required(CONF_WEBHOOK_ID): cv.string, +}) + + +async def _handle_webhook(action, hass, webhook_id, request): + """Handle incoming webhook.""" + result = { + 'platform': 'webhook', + 'webhook_id': webhook_id, + } + + if 'json' in request.headers.get(hdrs.CONTENT_TYPE, ''): + result['json'] = await request.json() + else: + result['data'] = await request.post() + + hass.async_run_job(action, {'trigger': result}) + + +async def async_trigger(hass, config, action): + """Trigger based on incoming webhooks.""" + webhook_id = config.get(CONF_WEBHOOK_ID) + hass.components.webhook.async_register( + webhook_id, partial(_handle_webhook, action)) + + @callback + def unregister(): + """Unregister webhook.""" + hass.components.webhook.async_unregister(webhook_id) + + return unregister diff --git a/tests/components/automation/test_webhook.py b/tests/components/automation/test_webhook.py new file mode 100644 index 00000000000..a6cde395583 --- /dev/null +++ b/tests/components/automation/test_webhook.py @@ -0,0 +1,75 @@ +"""The tests for the webhook automation trigger.""" +from homeassistant.core import callback +from homeassistant.setup import async_setup_component + + +async def test_webhook_json(hass, aiohttp_client): + """Test triggering with a JSON webhook.""" + events = [] + + @callback + def store_event(event): + """Helepr to store events.""" + events.append(event) + + hass.bus.async_listen('test_success', store_event) + + assert await async_setup_component(hass, 'automation', { + 'automation': { + 'trigger': { + 'platform': 'webhook', + 'webhook_id': 'json_webhook' + }, + 'action': { + 'event': 'test_success', + 'event_data_template': { + 'hello': 'yo {{ trigger.json.hello }}', + } + } + } + }) + + client = await aiohttp_client(hass.http.app) + + await client.post('/api/webhook/json_webhook', json={ + 'hello': 'world' + }) + + assert len(events) == 1 + assert events[0].data['hello'] == 'yo world' + + +async def test_webhook_post(hass, aiohttp_client): + """Test triggering with a POST webhook.""" + events = [] + + @callback + def store_event(event): + """Helepr to store events.""" + events.append(event) + + hass.bus.async_listen('test_success', store_event) + + assert await async_setup_component(hass, 'automation', { + 'automation': { + 'trigger': { + 'platform': 'webhook', + 'webhook_id': 'post_webhook' + }, + 'action': { + 'event': 'test_success', + 'event_data_template': { + 'hello': 'yo {{ trigger.data.hello }}', + } + } + } + }) + + client = await aiohttp_client(hass.http.app) + + await client.post('/api/webhook/post_webhook', data={ + 'hello': 'world' + }) + + assert len(events) == 1 + assert events[0].data['hello'] == 'yo world' From eb7db1f7632a4cb4ca4993d7f8b8e97ed49d4999 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 8 Oct 2018 20:50:24 +0200 Subject: [PATCH 230/247] block external IP (#17248) * block external IP * Update __init__.py --- .../components/emulated_hue/__init__.py | 15 ++++++++++-- .../components/emulated_hue/hue_api.py | 23 +++++++++++++++++++ tests/components/emulated_hue/test_hue_api.py | 10 ++++++++ 3 files changed, 46 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/emulated_hue/__init__.py b/homeassistant/components/emulated_hue/__init__.py index 8a67b933b9f..5f1d61dd602 100644 --- a/homeassistant/components/emulated_hue/__init__.py +++ b/homeassistant/components/emulated_hue/__init__.py @@ -18,6 +18,8 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.deprecation import get_deprecated import homeassistant.helpers.config_validation as cv from homeassistant.util.json import load_json, save_json +from homeassistant.components.http import real_ip + from .hue_api import ( HueUsernameView, HueAllLightsStateView, HueOneLightStateView, HueOneLightChangeView, HueGroupView) @@ -81,12 +83,20 @@ ATTR_EMULATED_HUE_NAME = 'emulated_hue_name' ATTR_EMULATED_HUE_HIDDEN = 'emulated_hue_hidden' -def setup(hass, yaml_config): +async def async_setup(hass, yaml_config): """Activate the emulated_hue component.""" config = Config(hass, yaml_config.get(DOMAIN, {})) app = web.Application() app['hass'] = hass + + real_ip.setup_real_ip(app, False, []) + # We misunderstood the startup signal. You're not allowed to change + # anything during startup. Temp workaround. + # pylint: disable=protected-access + app._on_startup.freeze() + await app.startup() + handler = None server = None @@ -131,7 +141,8 @@ def setup(hass, yaml_config): hass.bus.async_listen_once( EVENT_HOMEASSISTANT_STOP, stop_emulated_hue_bridge) - hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_emulated_hue_bridge) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, + start_emulated_hue_bridge) return True diff --git a/homeassistant/components/emulated_hue/hue_api.py b/homeassistant/components/emulated_hue/hue_api.py index c6fa622513b..3699a45ef30 100644 --- a/homeassistant/components/emulated_hue/hue_api.py +++ b/homeassistant/components/emulated_hue/hue_api.py @@ -20,6 +20,9 @@ from homeassistant.components.fan import ( SPEED_MEDIUM, SPEED_HIGH ) from homeassistant.components.http import HomeAssistantView +from homeassistant.components.http.const import KEY_REAL_IP +from homeassistant.util.network import is_local + _LOGGER = logging.getLogger(__name__) @@ -46,6 +49,10 @@ class HueUsernameView(HomeAssistantView): return self.json_message('devicetype not specified', HTTP_BAD_REQUEST) + if not is_local(request[KEY_REAL_IP]): + return self.json_message('only local IPs allowed', + HTTP_BAD_REQUEST) + return self.json([{'success': {'username': '12345678901234567890'}}]) @@ -63,6 +70,10 @@ class HueGroupView(HomeAssistantView): @core.callback def put(self, request, username): """Process a request to make the Logitech Pop working.""" + if not is_local(request[KEY_REAL_IP]): + return self.json_message('only local IPs allowed', + HTTP_BAD_REQUEST) + return self.json([{ 'error': { 'address': '/groups/0/action/scene', @@ -86,6 +97,10 @@ class HueAllLightsStateView(HomeAssistantView): @core.callback def get(self, request, username): """Process a request to get the list of available lights.""" + if not is_local(request[KEY_REAL_IP]): + return self.json_message('only local IPs allowed', + HTTP_BAD_REQUEST) + hass = request.app['hass'] json_response = {} @@ -114,6 +129,10 @@ class HueOneLightStateView(HomeAssistantView): @core.callback def get(self, request, username, entity_id): """Process a request to get the state of an individual light.""" + if not is_local(request[KEY_REAL_IP]): + return self.json_message('only local IPs allowed', + HTTP_BAD_REQUEST) + hass = request.app['hass'] entity_id = self.config.number_to_entity_id(entity_id) entity = hass.states.get(entity_id) @@ -146,6 +165,10 @@ class HueOneLightChangeView(HomeAssistantView): async def put(self, request, username, entity_number): """Process a request to set the state of an individual light.""" + if not is_local(request[KEY_REAL_IP]): + return self.json_message('only local IPs allowed', + HTTP_BAD_REQUEST) + config = self.config hass = request.app['hass'] entity_id = config.number_to_entity_id(entity_number) diff --git a/tests/components/emulated_hue/test_hue_api.py b/tests/components/emulated_hue/test_hue_api.py index 3920a45ddf6..8582f5b38cf 100644 --- a/tests/components/emulated_hue/test_hue_api.py +++ b/tests/components/emulated_hue/test_hue_api.py @@ -1,6 +1,7 @@ """The tests for the emulated Hue component.""" import asyncio import json +from ipaddress import ip_address from unittest.mock import patch from aiohttp.hdrs import CONTENT_TYPE @@ -484,3 +485,12 @@ def perform_put_light_state(hass_hue, client, entity_id, is_on, yield from hass_hue.async_block_till_done() return result + + +async def test_external_ip_blocked(hue_client): + """Test external IP blocked.""" + with patch('homeassistant.components.http.real_ip.ip_address', + return_value=ip_address('45.45.45.45')): + result = await hue_client.get('/api/username/lights') + + assert result.status == 400 From b59d69f3138747f83946ade64f937865014728f3 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Tue, 9 Oct 2018 10:11:14 +0200 Subject: [PATCH 231/247] Fix ambient light state of the Philips Eyecare Lamp (Closes: #16269) (#17259) --- homeassistant/components/light/xiaomi_miio.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/light/xiaomi_miio.py b/homeassistant/components/light/xiaomi_miio.py index 51c36fc2dd0..8bc2497a3e5 100644 --- a/homeassistant/components/light/xiaomi_miio.py +++ b/homeassistant/components/light/xiaomi_miio.py @@ -713,7 +713,7 @@ class XiaomiPhilipsEyecareLampAmbientLight(XiaomiPhilipsAbstractLight): _LOGGER.debug("Got new state: %s", state) self._available = True - self._state = state.eyecare + self._state = state.ambient self._brightness = ceil((255 / 100.0) * state.ambient_brightness) except DeviceException as ex: From 089e15e046df12d5708b5313fe6aca4e6256dbee Mon Sep 17 00:00:00 2001 From: Steven Looman Date: Tue, 9 Oct 2018 10:07:30 +0200 Subject: [PATCH 232/247] Add defaults, fixing #17229 (#17261) --- homeassistant/components/upnp/config_flow.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/upnp/config_flow.py b/homeassistant/components/upnp/config_flow.py index 885f2f64211..f695e3ada75 100644 --- a/homeassistant/components/upnp/config_flow.py +++ b/homeassistant/components/upnp/config_flow.py @@ -125,8 +125,8 @@ class UpnpFlowHandler(data_entry_flow.FlowHandler): data_schema=vol.Schema( OrderedDict([ (vol.Required('name'), vol.In(names)), - (vol.Optional('enable_sensors'), bool), - (vol.Optional('enable_port_mapping'), bool), + (vol.Optional('enable_sensors', default=False), bool), + (vol.Optional('enable_port_mapping', default=False), bool), ]) )) From 1a76603f531c3cb3f3bf7a140484e4735ebaea58 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Tue, 9 Oct 2018 10:06:42 +0200 Subject: [PATCH 233/247] Remove warning on script delay (#17264) * Remove warning on script delay * Use suppress --- homeassistant/helpers/script.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index ac53a3e32a2..5e660ba7b7f 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -1,6 +1,7 @@ """Helpers to execute scripts.""" import logging +from contextlib import suppress from itertools import islice from typing import Optional, Sequence @@ -95,7 +96,9 @@ class Script(): def async_script_delay(now): """Handle delay.""" # pylint: disable=cell-var-from-loop - self._async_remove_listener() + with suppress(ValueError): + self._async_listener.remove(unsub) + self.hass.async_create_task( self.async_run(variables, context)) @@ -240,7 +243,8 @@ class Script(): @callback def async_script_timeout(now): """Call after timeout is retrieve.""" - self._async_remove_listener() + with suppress(ValueError): + self._async_listener.remove(unsub) # Check if we want to continue to execute # the script after the timeout From a8a21ee28d0e468854b8f1a8a3ec700be481a235 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 9 Oct 2018 16:20:39 +0200 Subject: [PATCH 234/247] Bumped version to 0.80.0b4 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 628d418a735..e00abef3923 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 80 -PATCH_VERSION = '0b3' +PATCH_VERSION = '0b4' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From 83dec7173c56ad884f24262491798312c3922bef Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 10 Oct 2018 14:25:21 +0200 Subject: [PATCH 235/247] Update translations --- .../components/auth/.translations/fr.json | 16 ++++++++-- .../cast/.translations/zh-Hant.json | 2 +- .../components/deconz/.translations/ru.json | 2 +- .../homematicip_cloud/.translations/ru.json | 2 +- .../components/ifttt/.translations/fr.json | 18 ++++++++++++ .../components/ifttt/.translations/sl.json | 18 ++++++++++++ .../components/lifx/.translations/ca.json | 15 ++++++++++ .../components/lifx/.translations/fr.json | 15 ++++++++++ .../components/lifx/.translations/ko.json | 15 ++++++++++ .../components/lifx/.translations/lb.json | 15 ++++++++++ .../components/lifx/.translations/nl.json | 15 ++++++++++ .../components/lifx/.translations/pl.json | 15 ++++++++++ .../components/lifx/.translations/ru.json | 15 ++++++++++ .../components/lifx/.translations/sl.json | 15 ++++++++++ .../lifx/.translations/zh-Hant.json | 15 ++++++++++ .../components/mqtt/.translations/fr.json | 9 +++++- .../components/mqtt/.translations/sl.json | 7 +++++ .../components/smhi/.translations/ca.json | 19 ++++++++++++ .../components/smhi/.translations/en.json | 19 ++++++++++++ .../components/smhi/.translations/ko.json | 19 ++++++++++++ .../components/smhi/.translations/lb.json | 19 ++++++++++++ .../components/smhi/.translations/nl.json | 19 ++++++++++++ .../components/smhi/.translations/pl.json | 19 ++++++++++++ .../components/smhi/.translations/ru.json | 19 ++++++++++++ .../components/smhi/.translations/sl.json | 19 ++++++++++++ .../smhi/.translations/zh-Hant.json | 19 ++++++++++++ .../components/sonos/.translations/ko.json | 2 +- .../components/sonos/.translations/pl.json | 2 +- .../sonos/.translations/zh-Hant.json | 2 +- .../components/upnp/.translations/fr.json | 23 +++++++++++++++ .../components/upnp/.translations/nl.json | 6 +++- .../components/upnp/.translations/sl.json | 29 +++++++++++++++++++ .../components/zwave/.translations/ca.json | 17 +++++++++++ .../components/zwave/.translations/ko.json | 22 ++++++++++++++ .../components/zwave/.translations/lb.json | 22 ++++++++++++++ .../components/zwave/.translations/nl.json | 22 ++++++++++++++ .../components/zwave/.translations/ru.json | 22 ++++++++++++++ 37 files changed, 539 insertions(+), 10 deletions(-) create mode 100644 homeassistant/components/ifttt/.translations/fr.json create mode 100644 homeassistant/components/ifttt/.translations/sl.json create mode 100644 homeassistant/components/lifx/.translations/ca.json create mode 100644 homeassistant/components/lifx/.translations/fr.json create mode 100644 homeassistant/components/lifx/.translations/ko.json create mode 100644 homeassistant/components/lifx/.translations/lb.json create mode 100644 homeassistant/components/lifx/.translations/nl.json create mode 100644 homeassistant/components/lifx/.translations/pl.json create mode 100644 homeassistant/components/lifx/.translations/ru.json create mode 100644 homeassistant/components/lifx/.translations/sl.json create mode 100644 homeassistant/components/lifx/.translations/zh-Hant.json create mode 100644 homeassistant/components/smhi/.translations/ca.json create mode 100644 homeassistant/components/smhi/.translations/en.json create mode 100644 homeassistant/components/smhi/.translations/ko.json create mode 100644 homeassistant/components/smhi/.translations/lb.json create mode 100644 homeassistant/components/smhi/.translations/nl.json create mode 100644 homeassistant/components/smhi/.translations/pl.json create mode 100644 homeassistant/components/smhi/.translations/ru.json create mode 100644 homeassistant/components/smhi/.translations/sl.json create mode 100644 homeassistant/components/smhi/.translations/zh-Hant.json create mode 100644 homeassistant/components/upnp/.translations/fr.json create mode 100644 homeassistant/components/upnp/.translations/sl.json create mode 100644 homeassistant/components/zwave/.translations/ca.json create mode 100644 homeassistant/components/zwave/.translations/ko.json create mode 100644 homeassistant/components/zwave/.translations/lb.json create mode 100644 homeassistant/components/zwave/.translations/nl.json create mode 100644 homeassistant/components/zwave/.translations/ru.json diff --git a/homeassistant/components/auth/.translations/fr.json b/homeassistant/components/auth/.translations/fr.json index 85540314af0..cf0a1888495 100644 --- a/homeassistant/components/auth/.translations/fr.json +++ b/homeassistant/components/auth/.translations/fr.json @@ -1,11 +1,23 @@ { "mfa_setup": { "notify": { + "abort": { + "no_available_service": "Aucun service de notification disponible." + }, + "error": { + "invalid_code": "Code invalide. Veuillez essayer \u00e0 nouveau." + }, "step": { + "init": { + "description": "Veuillez s\u00e9lectionner l'un des services de notification:", + "title": "Configurer un mot de passe \u00e0 usage unique d\u00e9livr\u00e9 par le composant notify" + }, "setup": { - "description": "Un mot de passe unique a \u00e9t\u00e9 envoy\u00e9 par **notify.{notify_service}**. Veuillez le saisir ci-dessous :" + "description": "Un mot de passe unique a \u00e9t\u00e9 envoy\u00e9 par **notify.{notify_service}**. Veuillez le saisir ci-dessous :", + "title": "V\u00e9rifier la configuration" } - } + }, + "title": "Notifier un mot de passe unique" }, "totp": { "error": { diff --git a/homeassistant/components/cast/.translations/zh-Hant.json b/homeassistant/components/cast/.translations/zh-Hant.json index 711ac320397..d5383fb1a2b 100644 --- a/homeassistant/components/cast/.translations/zh-Hant.json +++ b/homeassistant/components/cast/.translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "no_devices_found": "\u5728\u7db2\u8def\u4e0a\u627e\u4e0d\u5230 Google Cast \u8a2d\u5099\u3002", + "no_devices_found": "\u5728\u7db2\u8def\u4e0a\u627e\u4e0d\u5230 Google Cast \u88dd\u7f6e\u3002", "single_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u6b21 Google Cast \u5373\u53ef\u3002" }, "step": { diff --git a/homeassistant/components/deconz/.translations/ru.json b/homeassistant/components/deconz/.translations/ru.json index a9b66314f31..4cbc9594ead 100644 --- a/homeassistant/components/deconz/.translations/ru.json +++ b/homeassistant/components/deconz/.translations/ru.json @@ -28,6 +28,6 @@ "title": "\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0434\u043b\u044f deCONZ" } }, - "title": "deCONZ" + "title": "\u041e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u0442\u044c \u0448\u043b\u044e\u0437 deCONZ" } } \ No newline at end of file diff --git a/homeassistant/components/homematicip_cloud/.translations/ru.json b/homeassistant/components/homematicip_cloud/.translations/ru.json index 6a0ea612fe8..ae67c616f3f 100644 --- a/homeassistant/components/homematicip_cloud/.translations/ru.json +++ b/homeassistant/components/homematicip_cloud/.translations/ru.json @@ -18,7 +18,7 @@ "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 (\u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u043a\u0430\u043a \u043f\u0440\u0435\u0444\u0438\u043a\u0441 \u0434\u043b\u044f \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u044f \u0432\u0441\u0435\u0445 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432)", "pin": "PIN-\u043a\u043e\u0434 (\u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e)" }, - "title": "\u0412\u044b\u0431\u0438\u0440\u0438\u0442\u0435 \u0442\u043e\u0447\u043a\u0443 \u0434\u043e\u0441\u0442\u0443\u043f\u0430 HomematicIP" + "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0442\u043e\u0447\u043a\u0443 \u0434\u043e\u0441\u0442\u0443\u043f\u0430 HomematicIP" }, "link": { "description": "\u041d\u0430\u0436\u043c\u0438\u0442\u0435 \u0441\u0438\u043d\u044e\u044e \u043a\u043d\u043e\u043f\u043a\u0443 \u043d\u0430 \u0442\u043e\u0447\u043a\u0435 \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u0438 \u043a\u043d\u043e\u043f\u043a\u0443 \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438, \u0447\u0442\u043e\u0431\u044b \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c HomematicIP \u0432 Home Assistant. \n\n ![\u0420\u0430\u0441\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u043a\u043d\u043e\u043f\u043a\u0438](/static/images/config_flows/config_homematicip_cloud.png)", diff --git a/homeassistant/components/ifttt/.translations/fr.json b/homeassistant/components/ifttt/.translations/fr.json new file mode 100644 index 00000000000..d083a624d70 --- /dev/null +++ b/homeassistant/components/ifttt/.translations/fr.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Votre instance Home Assistant doit \u00eatre accessible \u00e0 partir d'Internet pour recevoir les messages IFTTT.", + "one_instance_allowed": "Une seule instance est n\u00e9cessaire." + }, + "create_entry": { + "default": "Pour envoyer des \u00e9v\u00e9nements \u00e0 Home Assistant, vous devez utiliser l'action \"Effectuer une demande Web\" \u00e0 partir de [l'applet IFTTT Webhook] ( {applet_url} ). \n\n Remplissez les informations suivantes: \n\n - URL: ` {webhook_url} ` \n - M\u00e9thode: POST \n - Type de contenu: application / json \n\n Voir [la documentation] ( {docs_url} ) pour savoir comment configurer les automatisations pour g\u00e9rer les donn\u00e9es entrantes." + }, + "step": { + "user": { + "description": "\u00cates-vous s\u00fbr de vouloir configurer IFTTT?", + "title": "Configurer l'applet IFTTT Webhook" + } + }, + "title": "IFTTT" + } +} \ No newline at end of file diff --git a/homeassistant/components/ifttt/.translations/sl.json b/homeassistant/components/ifttt/.translations/sl.json new file mode 100644 index 00000000000..f5cc1dc572e --- /dev/null +++ b/homeassistant/components/ifttt/.translations/sl.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Va\u0161 Home Assistent mora biti dostopek prek interneta, da boste lahko prejemali IFTTT sporo\u010dila.", + "one_instance_allowed": "Potrebna je samo ena instanca." + }, + "create_entry": { + "default": "\u010ce \u017eelite poslati dogodke Home Assistent-u, boste morali uporabiti akcijo \u00bbNaredi spletno zahtevo\u00ab iz orodja [IFTTT Webhook applet] ( {applet_url} ). \n\n Izpolnite naslednje podatke: \n\n - URL: ` {webhook_url} ` \n - Metoda: POST \n - Vrsta vsebine: application/json \n\n Poglejte si [dokumentacijo] ( {docs_url} ) o tem, kako konfigurirati avtomatizacijo za obdelavo dohodnih podatkov." + }, + "step": { + "user": { + "description": "Ali ste prepri\u010dani, da \u017eelite nastaviti IFTTT?", + "title": "Nastavite IFTTT Webhook Applet" + } + }, + "title": "IFTTT" + } +} \ No newline at end of file diff --git a/homeassistant/components/lifx/.translations/ca.json b/homeassistant/components/lifx/.translations/ca.json new file mode 100644 index 00000000000..b3896d49e1d --- /dev/null +++ b/homeassistant/components/lifx/.translations/ca.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "No s'han trobat dispositius LIFX a la xarxa.", + "single_instance_allowed": "Nom\u00e9s \u00e9s possible una \u00fanica configuraci\u00f3 de LIFX." + }, + "step": { + "confirm": { + "description": "Voleu configurar LIFX?", + "title": "LIFX" + } + }, + "title": "LIFX" + } +} \ No newline at end of file diff --git a/homeassistant/components/lifx/.translations/fr.json b/homeassistant/components/lifx/.translations/fr.json new file mode 100644 index 00000000000..96a264fa6b2 --- /dev/null +++ b/homeassistant/components/lifx/.translations/fr.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Aucun p\u00e9riph\u00e9rique LIFX trouv\u00e9 sur le r\u00e9seau.", + "single_instance_allowed": "Une seule configuration de LIFX est possible." + }, + "step": { + "confirm": { + "description": "Voulez-vous configurer LIFX?", + "title": "LIFX" + } + }, + "title": "LIFX" + } +} \ No newline at end of file diff --git a/homeassistant/components/lifx/.translations/ko.json b/homeassistant/components/lifx/.translations/ko.json new file mode 100644 index 00000000000..c795c54badb --- /dev/null +++ b/homeassistant/components/lifx/.translations/ko.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "LIFX \uc7a5\uce58\uac00 \ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \ubc1c\uacac\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4.", + "single_instance_allowed": "\ud558\ub098\uc758 LIFX \ub9cc \uad6c\uc131 \ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4." + }, + "step": { + "confirm": { + "description": "LIFX \ub97c \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "title": "LIFX" + } + }, + "title": "LIFX" + } +} \ No newline at end of file diff --git a/homeassistant/components/lifx/.translations/lb.json b/homeassistant/components/lifx/.translations/lb.json new file mode 100644 index 00000000000..2e033280e46 --- /dev/null +++ b/homeassistant/components/lifx/.translations/lb.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Keng LIFX Apparater am Netzwierk fonnt.", + "single_instance_allowed": "N\u00ebmmen eng eenzeg Konfiguratioun vun LIFX ass erlaabt." + }, + "step": { + "confirm": { + "description": "Soll LIFX konfigur\u00e9iert ginn?", + "title": "LIFX" + } + }, + "title": "LIFX" + } +} \ No newline at end of file diff --git a/homeassistant/components/lifx/.translations/nl.json b/homeassistant/components/lifx/.translations/nl.json new file mode 100644 index 00000000000..a23502729d6 --- /dev/null +++ b/homeassistant/components/lifx/.translations/nl.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Geen LIFX-apparaten gevonden op het netwerk.", + "single_instance_allowed": "Slechts een enkele configuratie van LIFX is mogelijk." + }, + "step": { + "confirm": { + "description": "Wilt u LIFX instellen?", + "title": "LIFX" + } + }, + "title": "LIFX" + } +} \ No newline at end of file diff --git a/homeassistant/components/lifx/.translations/pl.json b/homeassistant/components/lifx/.translations/pl.json new file mode 100644 index 00000000000..f13c0b54bbd --- /dev/null +++ b/homeassistant/components/lifx/.translations/pl.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Nie znaleziono w sieci urz\u0105dze\u0144 LIFX.", + "single_instance_allowed": "Wymagana jest tylko jedna konfiguracja LIFX." + }, + "step": { + "confirm": { + "description": "Czy chcesz skonfigurowa\u0107 LIFX?", + "title": "LIFX" + } + }, + "title": "LIFX" + } +} \ No newline at end of file diff --git a/homeassistant/components/lifx/.translations/ru.json b/homeassistant/components/lifx/.translations/ru.json new file mode 100644 index 00000000000..5ad351b7a90 --- /dev/null +++ b/homeassistant/components/lifx/.translations/ru.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 LIFX \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b \u0432 \u0441\u0435\u0442\u0438.", + "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 LIFX?", + "title": "LIFX" + } + }, + "title": "LIFX" + } +} \ No newline at end of file diff --git a/homeassistant/components/lifx/.translations/sl.json b/homeassistant/components/lifx/.translations/sl.json new file mode 100644 index 00000000000..492bf9010dd --- /dev/null +++ b/homeassistant/components/lifx/.translations/sl.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "V omre\u017eju ni najdenih naprav LIFX.", + "single_instance_allowed": "Mo\u017ena je samo ena konfiguracija LIFX-a." + }, + "step": { + "confirm": { + "description": "Ali \u017eelite nastaviti LIFX?", + "title": "LIFX" + } + }, + "title": "LIFX" + } +} \ No newline at end of file diff --git a/homeassistant/components/lifx/.translations/zh-Hant.json b/homeassistant/components/lifx/.translations/zh-Hant.json new file mode 100644 index 00000000000..4c66f0d0133 --- /dev/null +++ b/homeassistant/components/lifx/.translations/zh-Hant.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u5728\u7db2\u8def\u4e0a\u627e\u4e0d\u5230 LIFX \u88dd\u7f6e\u3002", + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44 LIFX\u3002" + }, + "step": { + "confirm": { + "description": "\u662f\u5426\u8981\u8a2d\u5b9a LIFX\uff1f", + "title": "LIFX" + } + }, + "title": "LIFX" + } +} \ No newline at end of file diff --git a/homeassistant/components/mqtt/.translations/fr.json b/homeassistant/components/mqtt/.translations/fr.json index 916b4fdaf39..648c2f972d7 100644 --- a/homeassistant/components/mqtt/.translations/fr.json +++ b/homeassistant/components/mqtt/.translations/fr.json @@ -10,13 +10,20 @@ "broker": { "data": { "broker": "Broker", - "discovery": "Activer la d\u00e9couverte automatique", + "discovery": "Activer la d\u00e9couverte", "password": "Mot de passe", "port": "Port", "username": "Nom d'utilisateur" }, "description": "Veuillez entrer les informations de connexion de votre broker MQTT.", "title": "MQTT" + }, + "hassio_confirm": { + "data": { + "discovery": "Activer la d\u00e9couverte" + }, + "description": "Vous voulez configurer Home Assistant pour vous connecter au broker MQTT fourni par l\u2019Add-on hass.io {addon} ?", + "title": "MQTT Broker via le module compl\u00e9mentaire Hass.io" } }, "title": "MQTT" diff --git a/homeassistant/components/mqtt/.translations/sl.json b/homeassistant/components/mqtt/.translations/sl.json index a12498ac4c2..d8d331449a2 100644 --- a/homeassistant/components/mqtt/.translations/sl.json +++ b/homeassistant/components/mqtt/.translations/sl.json @@ -17,6 +17,13 @@ }, "description": "Prosimo vnesite informacije o povezavi va\u0161ega MQTT posrednika.", "title": "MQTT" + }, + "hassio_confirm": { + "data": { + "discovery": "Omogo\u010di odkrivanje" + }, + "description": "\u017delite konfigurirati Home Assistent-a za povezavo s posrednikom MQTT, ki ga ponuja hass.io add-on {addon} ?", + "title": "MQTT Broker prek dodatka Hass.io" } }, "title": "MQTT" diff --git a/homeassistant/components/smhi/.translations/ca.json b/homeassistant/components/smhi/.translations/ca.json new file mode 100644 index 00000000000..23b6a2934f0 --- /dev/null +++ b/homeassistant/components/smhi/.translations/ca.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "name_exists": "El nom ja existeix", + "wrong_location": "Ubicaci\u00f3 nom\u00e9s a Su\u00e8cia" + }, + "step": { + "user": { + "data": { + "latitude": "Latitud", + "longitude": "Longitud", + "name": "Nom" + }, + "title": "Ubicaci\u00f3 a Su\u00e8cia" + } + }, + "title": "Servei meteorol\u00f2gic suec (SMHI)" + } +} \ No newline at end of file diff --git a/homeassistant/components/smhi/.translations/en.json b/homeassistant/components/smhi/.translations/en.json new file mode 100644 index 00000000000..6aa256d87d4 --- /dev/null +++ b/homeassistant/components/smhi/.translations/en.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "name_exists": "Name already exists", + "wrong_location": "Location Sweden only" + }, + "step": { + "user": { + "data": { + "latitude": "Latitude", + "longitude": "Longitude", + "name": "Name" + }, + "title": "Location in Sweden" + } + }, + "title": "Swedish weather service (SMHI)" + } +} \ No newline at end of file diff --git a/homeassistant/components/smhi/.translations/ko.json b/homeassistant/components/smhi/.translations/ko.json new file mode 100644 index 00000000000..f307fa1ad23 --- /dev/null +++ b/homeassistant/components/smhi/.translations/ko.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "name_exists": "\uc774\ub984\uc774 \uc774\ubbf8 \uc874\uc7ac\ud569\ub2c8\ub2e4", + "wrong_location": "\uc2a4\uc6e8\ub374 \uc9c0\uc5ed \uc804\uc6a9\uc785\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "latitude": "\uc704\ub3c4", + "longitude": "\uacbd\ub3c4", + "name": "\uc774\ub984" + }, + "title": "\uc2a4\uc6e8\ub374 \uc9c0\uc5ed \uc704\uce58" + } + }, + "title": "\uc2a4\uc6e8\ub374 \uae30\uc0c1 \uc11c\ube44\uc2a4 (SMHI)" + } +} \ No newline at end of file diff --git a/homeassistant/components/smhi/.translations/lb.json b/homeassistant/components/smhi/.translations/lb.json new file mode 100644 index 00000000000..46abfd2677f --- /dev/null +++ b/homeassistant/components/smhi/.translations/lb.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "name_exists": "Numm g\u00ebtt et schonn", + "wrong_location": "N\u00ebmmen Uertschaften an Schweden" + }, + "step": { + "user": { + "data": { + "latitude": "Breedegrad", + "longitude": "L\u00e4ngegrad", + "name": "Numm" + }, + "title": "Uertschaft an Schweden" + } + }, + "title": "Schwedeschen Wieder D\u00e9ngscht (SMHI)" + } +} \ No newline at end of file diff --git a/homeassistant/components/smhi/.translations/nl.json b/homeassistant/components/smhi/.translations/nl.json new file mode 100644 index 00000000000..88edc116e74 --- /dev/null +++ b/homeassistant/components/smhi/.translations/nl.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "name_exists": "Naam bestaat al", + "wrong_location": "Locatie alleen Zweden" + }, + "step": { + "user": { + "data": { + "latitude": "Breedtegraad", + "longitude": "Lengtegraad", + "name": "Naam" + }, + "title": "Locatie in Zweden" + } + }, + "title": "Zweedse weerdienst (SMHI)" + } +} \ No newline at end of file diff --git a/homeassistant/components/smhi/.translations/pl.json b/homeassistant/components/smhi/.translations/pl.json new file mode 100644 index 00000000000..21973cd54b6 --- /dev/null +++ b/homeassistant/components/smhi/.translations/pl.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "name_exists": "Nazwa ju\u017c istnieje", + "wrong_location": "Lokalizacja w Szwecji" + }, + "step": { + "user": { + "data": { + "latitude": "Szeroko\u015b\u0107 geograficzna", + "longitude": "D\u0142ugo\u015b\u0107 geograficzna", + "name": "Nazwa" + }, + "title": "Lokalizacja w Szwecji" + } + }, + "title": "Szwedzka us\u0142uga pogodowa (SMHI)" + } +} \ No newline at end of file diff --git a/homeassistant/components/smhi/.translations/ru.json b/homeassistant/components/smhi/.translations/ru.json new file mode 100644 index 00000000000..012bb74c568 --- /dev/null +++ b/homeassistant/components/smhi/.translations/ru.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "name_exists": "\u0418\u043c\u044f \u0443\u0436\u0435 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u0435\u0442", + "wrong_location": "\u0422\u043e\u043b\u044c\u043a\u043e \u0434\u043b\u044f \u0428\u0432\u0435\u0446\u0438\u0438" + }, + "step": { + "user": { + "data": { + "latitude": "\u0428\u0438\u0440\u043e\u0442\u0430", + "longitude": "\u0414\u043e\u043b\u0433\u043e\u0442\u0430", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435" + }, + "title": "\u041c\u0435\u0441\u0442\u043e\u043d\u0430\u0445\u043e\u0436\u0434\u0435\u043d\u0438\u0435 \u0432 \u0428\u0432\u0435\u0446\u0438\u0438" + } + }, + "title": "\u0428\u0432\u0435\u0434\u0441\u043a\u0430\u044f \u043c\u0435\u0442\u0435\u043e\u0440\u043e\u043b\u043e\u0433\u0438\u0447\u0435\u0441\u043a\u0430\u044f \u0441\u043b\u0443\u0436\u0431\u0430 (SMHI)" + } +} \ No newline at end of file diff --git a/homeassistant/components/smhi/.translations/sl.json b/homeassistant/components/smhi/.translations/sl.json new file mode 100644 index 00000000000..94c3750f06f --- /dev/null +++ b/homeassistant/components/smhi/.translations/sl.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "name_exists": "Ime \u017ee obstaja", + "wrong_location": "Lokacija le na \u0160vedskem" + }, + "step": { + "user": { + "data": { + "latitude": "Zemljepisna \u0161irina", + "longitude": "Zemljepisna dol\u017eina", + "name": "Ime" + }, + "title": "Lokacija na \u0160vedskem" + } + }, + "title": "\u0160vedska vremenska slu\u017eba (SMHI)" + } +} \ No newline at end of file diff --git a/homeassistant/components/smhi/.translations/zh-Hant.json b/homeassistant/components/smhi/.translations/zh-Hant.json new file mode 100644 index 00000000000..b982baac2f8 --- /dev/null +++ b/homeassistant/components/smhi/.translations/zh-Hant.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "name_exists": "\u8a72\u540d\u7a31\u5df2\u5b58\u5728", + "wrong_location": "\u50c5\u9650\u745e\u5178\u5ea7\u6a19" + }, + "step": { + "user": { + "data": { + "latitude": "\u7def\u5ea6", + "longitude": "\u7d93\u5ea6", + "name": "\u540d\u7a31" + }, + "title": "\u745e\u5178\u5ea7\u6a19" + } + }, + "title": "\u745e\u5178\u6c23\u8c61\u670d\u52d9\uff08SMHI\uff09" + } +} \ No newline at end of file diff --git a/homeassistant/components/sonos/.translations/ko.json b/homeassistant/components/sonos/.translations/ko.json index 89933f57425..0b2e2a1875c 100644 --- a/homeassistant/components/sonos/.translations/ko.json +++ b/homeassistant/components/sonos/.translations/ko.json @@ -6,7 +6,7 @@ }, "step": { "confirm": { - "description": "Sonos\ub97c \uc124\uc815 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "description": "Sonos \ub97c \uc124\uc815 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", "title": "Sonos" } }, diff --git a/homeassistant/components/sonos/.translations/pl.json b/homeassistant/components/sonos/.translations/pl.json index 2a0c526b9a6..a45cb4e9824 100644 --- a/homeassistant/components/sonos/.translations/pl.json +++ b/homeassistant/components/sonos/.translations/pl.json @@ -6,7 +6,7 @@ }, "step": { "confirm": { - "description": "Chcesz skonfigurowa\u0107 Sonos?", + "description": "Czy chcesz skonfigurowa\u0107 Sonos?", "title": "Sonos" } }, diff --git a/homeassistant/components/sonos/.translations/zh-Hant.json b/homeassistant/components/sonos/.translations/zh-Hant.json index c6fb13c3605..520a29b7602 100644 --- a/homeassistant/components/sonos/.translations/zh-Hant.json +++ b/homeassistant/components/sonos/.translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "no_devices_found": "\u5728\u7db2\u8def\u4e0a\u627e\u4e0d\u5230 Sonos \u8a2d\u5099\u3002", + "no_devices_found": "\u5728\u7db2\u8def\u4e0a\u627e\u4e0d\u5230 Sonos \u88dd\u7f6e\u3002", "single_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u6b21 Sonos \u5373\u53ef\u3002" }, "step": { diff --git a/homeassistant/components/upnp/.translations/fr.json b/homeassistant/components/upnp/.translations/fr.json new file mode 100644 index 00000000000..3eac9577890 --- /dev/null +++ b/homeassistant/components/upnp/.translations/fr.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "UPnP / IGD est d\u00e9j\u00e0 configur\u00e9", + "no_devices_discovered": "Aucun UPnP / IGD d\u00e9couvert", + "no_sensors_or_port_mapping": "Activer au moins les capteurs ou la cartographie des ports" + }, + "step": { + "init": { + "title": "UPnP / IGD" + }, + "user": { + "data": { + "enable_port_mapping": "Activer le mappage de port pour Home Assistant", + "enable_sensors": "Ajouter des capteurs de trafic", + "igd": "UPnP / IGD" + }, + "title": "Options de configuration pour UPnP / IGD" + } + }, + "title": "UPnP / IGD" + } +} \ No newline at end of file diff --git a/homeassistant/components/upnp/.translations/nl.json b/homeassistant/components/upnp/.translations/nl.json index c6356709170..647eb647f24 100644 --- a/homeassistant/components/upnp/.translations/nl.json +++ b/homeassistant/components/upnp/.translations/nl.json @@ -6,10 +6,14 @@ "no_sensors_or_port_mapping": "Schakel ten minste sensoren of poorttoewijzing in" }, "step": { + "init": { + "title": "UPnP/IGD" + }, "user": { "data": { "enable_port_mapping": "Poorttoewijzing voor Home Assistant inschakelen", - "enable_sensors": "Voeg verkeerssensoren toe" + "enable_sensors": "Voeg verkeerssensoren toe", + "igd": "UPnP/IGD" }, "title": "Configuratiemogelijkheden voor de UPnP/IGD" } diff --git a/homeassistant/components/upnp/.translations/sl.json b/homeassistant/components/upnp/.translations/sl.json new file mode 100644 index 00000000000..20debe7f09a --- /dev/null +++ b/homeassistant/components/upnp/.translations/sl.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "UPnP/IGD je \u017ee konfiguriran", + "no_devices_discovered": "Ni odkritih UPnP/IGD naprav", + "no_sensors_or_port_mapping": "Omogo\u010dite vsaj senzorje ali preslikavo vrat (port mapping)" + }, + "error": { + "few": "nekaj", + "one": "ena", + "other": "ve\u010d", + "two": "dve" + }, + "step": { + "init": { + "title": "UPnP/IGD" + }, + "user": { + "data": { + "enable_port_mapping": "Omogo\u010dajo preslikavo vrat (port mapping) za Home Assistent-a", + "enable_sensors": "Dodaj prometne senzorje", + "igd": "UPnP/IGD" + }, + "title": "Mo\u017enosti konfiguracije za UPnP/IGD" + } + }, + "title": "UPnP/IGD" + } +} \ No newline at end of file diff --git a/homeassistant/components/zwave/.translations/ca.json b/homeassistant/components/zwave/.translations/ca.json new file mode 100644 index 00000000000..8c39ca58201 --- /dev/null +++ b/homeassistant/components/zwave/.translations/ca.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "Z-Wave ja est\u00e0 configurat", + "one_instance_only": "El component nom\u00e9s admet una inst\u00e0ncia de Z-Wave" + }, + "step": { + "user": { + "data": { + "network_key": "Clau de xarxa (deixeu-ho en blanc per generar-la autom\u00e0ticament)" + }, + "title": "Configureu Z-Wave" + } + }, + "title": "Z-Wave" + } +} \ No newline at end of file diff --git a/homeassistant/components/zwave/.translations/ko.json b/homeassistant/components/zwave/.translations/ko.json new file mode 100644 index 00000000000..43103de3d51 --- /dev/null +++ b/homeassistant/components/zwave/.translations/ko.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Z-Wave \uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "one_instance_only": "\uad6c\uc131\uc694\uc18c\ub294 \ud558\ub098\uc758 Z-Wave \uc778\uc2a4\ud134\uc2a4\ub9cc \uc9c0\uc6d0\ud569\ub2c8\ub2e4" + }, + "error": { + "option_error": "Z-Wave \uc720\ud6a8\uc131 \uac80\uc0ac\uc5d0 \uc2e4\ud328\ud588\uc2b5\ub2c8\ub2e4. USB \uc2a4\ud2f1\uc758 \uacbd\ub85c\uac00 \uc815\ud655\ud569\ub2c8\uae4c?" + }, + "step": { + "user": { + "data": { + "network_key": "\ub124\ud2b8\uc6cc\ud06c \ud0a4 (\uacf5\ub780\uc73c\ub85c \ube44\uc6cc\ub450\uba74 \uc790\ub3d9 \uc0dd\uc131\ud569\ub2c8\ub2e4)", + "usb_path": "USB \uacbd\ub85c" + }, + "description": "\uad6c\uc131 \ubcc0\uc218\uc5d0 \ub300\ud55c \uc815\ubcf4\ub294 https://www.home-assistant.io/docs/z-wave/installation/ \uc744 \ucc38\uc870\ud574\uc8fc\uc138\uc694", + "title": "Z-Wave \uc124\uc815" + } + }, + "title": "Z-Wave" + } +} \ No newline at end of file diff --git a/homeassistant/components/zwave/.translations/lb.json b/homeassistant/components/zwave/.translations/lb.json new file mode 100644 index 00000000000..84b6d8aa67d --- /dev/null +++ b/homeassistant/components/zwave/.translations/lb.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Z-Wave ass scho konfigur\u00e9iert", + "one_instance_only": "Komponent \u00ebnnerst\u00ebtzt n\u00ebmmen eng Z-Wave Instanz" + }, + "error": { + "option_error": "Z-Wave Validatioun net g\u00eblteg. Ass de Pad zum USB Stick richteg?" + }, + "step": { + "user": { + "data": { + "network_key": "Netzwierk Schl\u00ebssel (eidel loossen fir een automatesch z'erstellen)", + "usb_path": "USB Pad" + }, + "description": "Lies op https://www.home-assistant.io/docs/z-wave/installation/ fir weider Informatiounen iwwert d'Konfiguratioun vun den Variabelen", + "title": "Z-Wave konfigur\u00e9ieren" + } + }, + "title": "Z-Wave" + } +} \ No newline at end of file diff --git a/homeassistant/components/zwave/.translations/nl.json b/homeassistant/components/zwave/.translations/nl.json new file mode 100644 index 00000000000..0b700b895fd --- /dev/null +++ b/homeassistant/components/zwave/.translations/nl.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Z-Wave is al geconfigureerd", + "one_instance_only": "Component ondersteunt slechts \u00e9\u00e9n Z-Wave-instantie" + }, + "error": { + "option_error": "Z-Wave-validatie mislukt. Is het pad naar de USB-stick correct?" + }, + "step": { + "user": { + "data": { + "network_key": "Netwerksleutel (laat leeg om automatisch te genereren)", + "usb_path": "USB-pad" + }, + "description": "Zie https://www.home-assistant.io/docs/z-wave/installation/ voor informatie over de configuratievariabelen", + "title": "Stel Z-Wave in" + } + }, + "title": "Z-Wave" + } +} \ No newline at end of file diff --git a/homeassistant/components/zwave/.translations/ru.json b/homeassistant/components/zwave/.translations/ru.json new file mode 100644 index 00000000000..457bfd3baa8 --- /dev/null +++ b/homeassistant/components/zwave/.translations/ru.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "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.", + "one_instance_only": "\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u0438\u043d \u044d\u043a\u0437\u0435\u043c\u043f\u043b\u044f\u0440 Z-Wave" + }, + "error": { + "option_error": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438 Z-Wave. \u041f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u043f\u0443\u0442\u044c \u043a USB-\u043d\u0430\u043a\u043e\u043f\u0438\u0442\u0435\u043b\u044e." + }, + "step": { + "user": { + "data": { + "network_key": "\u041a\u043b\u044e\u0447 \u0441\u0435\u0442\u0438 (\u043e\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u043f\u043e\u043b\u0435 \u043f\u0443\u0441\u0442\u044b\u043c \u0434\u043b\u044f \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u043e\u0439 \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0438)", + "usb_path": "\u041f\u0443\u0442\u044c \u043a USB" + }, + "description": "\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](https://www.home-assistant.io/docs/z-wave/installation/) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438 \u043e \u043f\u0435\u0440\u0435\u043c\u0435\u043d\u043d\u044b\u0445 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438", + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 Z-Wave" + } + }, + "title": "Z-Wave" + } +} \ No newline at end of file From 3f28b30860a8b9bd24d8ad54396afca0e83cf473 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Wed, 10 Oct 2018 14:07:51 +0200 Subject: [PATCH 236/247] Hassio auth (#17274) * Create auth.py * Update auth.py * Update auth.py * Update __init__.py * Update auth.py * Update auth.py * Update auth.py * Update auth.py * Update auth.py * Update auth.py * Update auth.py * Update auth.py * Update auth.py * Add tests * Update test_auth.py * Update auth.py * Update test_auth.py * Update auth.py --- homeassistant/components/hassio/__init__.py | 4 + homeassistant/components/hassio/auth.py | 75 ++++++++++++++++ tests/components/hassio/test_auth.py | 95 +++++++++++++++++++++ 3 files changed, 174 insertions(+) create mode 100644 homeassistant/components/hassio/auth.py create mode 100644 tests/components/hassio/test_auth.py diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index b97d748d864..9516675480a 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -19,6 +19,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.loader import bind_hass from homeassistant.util.dt import utcnow +from .auth import async_setup_auth from .handler import HassIO, HassioAPIError from .discovery import async_setup_discovery from .http import HassIOView @@ -280,4 +281,7 @@ async def async_setup(hass, config): # Init discovery Hass.io feature async_setup_discovery(hass, hassio, config) + # Init auth Hass.io feature + async_setup_auth(hass) + return True diff --git a/homeassistant/components/hassio/auth.py b/homeassistant/components/hassio/auth.py new file mode 100644 index 00000000000..73ef5aa29cc --- /dev/null +++ b/homeassistant/components/hassio/auth.py @@ -0,0 +1,75 @@ +"""Implement the auth feature from Hass.io for Add-ons.""" +import logging +from ipaddress import ip_address +import os + +from aiohttp import web +from aiohttp.web_exceptions import ( + HTTPForbidden, HTTPNotFound, HTTPUnauthorized) +import voluptuous as vol + +from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv +from homeassistant.exceptions import HomeAssistantError +from homeassistant.components.http import HomeAssistantView +from homeassistant.components.http.const import KEY_REAL_IP +from homeassistant.components.http.data_validator import RequestDataValidator + +_LOGGER = logging.getLogger(__name__) + +ATTR_USERNAME = 'username' +ATTR_PASSWORD = 'password' + + +SCHEMA_API_AUTH = vol.Schema({ + vol.Required(ATTR_USERNAME): cv.string, + vol.Required(ATTR_PASSWORD): cv.string, +}) + + +@callback +def async_setup_auth(hass): + """Auth setup.""" + hassio_auth = HassIOAuth(hass) + hass.http.register_view(hassio_auth) + + +class HassIOAuth(HomeAssistantView): + """Hass.io view to handle base part.""" + + name = "api:hassio_auth" + url = "/api/hassio_auth" + + def __init__(self, hass): + """Initialize WebView.""" + self.hass = hass + + @RequestDataValidator(SCHEMA_API_AUTH) + async def post(self, request, data): + """Handle new discovery requests.""" + hassio_ip = os.environ['HASSIO'].split(':')[0] + if request[KEY_REAL_IP] != ip_address(hassio_ip): + _LOGGER.error( + "Invalid auth request from %s", request[KEY_REAL_IP]) + raise HTTPForbidden() + + await self._check_login(data[ATTR_USERNAME], data[ATTR_PASSWORD]) + return web.Response(status=200) + + def _get_provider(self): + """Return Homeassistant auth provider.""" + for prv in self.hass.auth.auth_providers: + if prv.type == 'homeassistant': + return prv + + _LOGGER.error("Can't find Home Assistant auth.") + raise HTTPNotFound() + + async def _check_login(self, username, password): + """Check User credentials.""" + provider = self._get_provider() + + try: + await provider.async_validate_login(username, password) + except HomeAssistantError: + raise HTTPUnauthorized() from None diff --git a/tests/components/hassio/test_auth.py b/tests/components/hassio/test_auth.py new file mode 100644 index 00000000000..50d9488a19c --- /dev/null +++ b/tests/components/hassio/test_auth.py @@ -0,0 +1,95 @@ +"""The tests for the hassio component.""" +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 . 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: + resp = await hassio_client.post( + '/api/hassio_auth', + json={ + "username": "test", + "password": "123456" + }, + headers={ + HTTP_HEADER_HA_AUTH: API_PASSWORD + } + ) + + # Check we got right response + assert resp.status == 200 + mock_login.assert_called_with("test", "123456") + + +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: + resp = await hassio_client.post( + '/api/hassio_auth', + json={ + "username": "test", + "password": "123456" + }, + headers={ + HTTP_HEADER_HA_AUTH: API_PASSWORD + } + ) + + # Check we got right response + assert resp.status == 401 + mock_login.assert_called_with("test", "123456") + + +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: + resp = await hassio_client.post( + '/api/hassio_auth', + headers={ + HTTP_HEADER_HA_AUTH: API_PASSWORD + } + ) + + # Check we got right response + assert resp.status == 400 + assert not mock_login.called + + +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: + resp = await hassio_client.post( + '/api/hassio_auth', + json={ + "password": "123456" + }, + headers={ + HTTP_HEADER_HA_AUTH: API_PASSWORD + } + ) + + # Check we got right response + assert resp.status == 400 + assert not mock_login.called From 951d7154b8fa1c532aafc318eff19a07d4cf6a08 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Wed, 10 Oct 2018 13:50:11 +0200 Subject: [PATCH 237/247] Fix hassio discovery (#17275) * Update discovery.py * Update test_discovery.py * Update test_discovery.py * Update test_discovery.py * Update test_discovery.py * Update test_discovery.py * Update test_discovery.py * Fix tests * fix lint --- homeassistant/components/hassio/discovery.py | 9 ++-- tests/components/hassio/conftest.py | 2 + tests/components/hassio/test_discovery.py | 53 ++++++++++++++++++++ 3 files changed, 61 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/hassio/discovery.py b/homeassistant/components/hassio/discovery.py index ba5c8c3f789..3c5242607c1 100644 --- a/homeassistant/components/hassio/discovery.py +++ b/homeassistant/components/hassio/discovery.py @@ -5,7 +5,7 @@ import logging from aiohttp import web from aiohttp.web_exceptions import HTTPServiceUnavailable -from homeassistant.core import callback +from homeassistant.core import callback, CoreState from homeassistant.const import EVENT_HOMEASSISTANT_START from homeassistant.components.http import HomeAssistantView @@ -40,8 +40,11 @@ def async_setup_discovery(hass, hassio, config): if jobs: await asyncio.wait(jobs) - hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_START, async_discovery_start_handler) + if hass.state == CoreState.running: + hass.async_create_task(async_discovery_start_handler(None)) + else: + hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_START, async_discovery_start_handler) hass.http.register_view(hassio_discovery) diff --git a/tests/components/hassio/conftest.py b/tests/components/hassio/conftest.py index fb3a172a45c..f9ad1c578de 100644 --- a/tests/components/hassio/conftest.py +++ b/tests/components/hassio/conftest.py @@ -4,6 +4,7 @@ from unittest.mock import patch, Mock import pytest +from homeassistant.core import CoreState from homeassistant.setup import async_setup_component from homeassistant.components.hassio.handler import HassIO, HassioAPIError @@ -33,6 +34,7 @@ def hassio_client(hassio_env, hass, aiohttp_client): patch('homeassistant.components.hassio.HassIO.' 'get_homeassistant_info', Mock(side_effect=HassioAPIError())): + hass.state = CoreState.starting hass.loop.run_until_complete(async_setup_component(hass, 'hassio', { 'http': { 'api_password': API_PASSWORD diff --git a/tests/components/hassio/test_discovery.py b/tests/components/hassio/test_discovery.py index 98d0835c102..c8926a1cd18 100644 --- a/tests/components/hassio/test_discovery.py +++ b/tests/components/hassio/test_discovery.py @@ -1,6 +1,8 @@ """Test config flow.""" from unittest.mock import patch, Mock +from homeassistant.setup import async_setup_component +from homeassistant.components.hassio.handler import HassioAPIError from homeassistant.const import EVENT_HOMEASSISTANT_START, HTTP_HEADER_HA_AUTH from tests.common import mock_coro @@ -29,6 +31,8 @@ async def test_hassio_discovery_startup(hass, aioclient_mock, hassio_client): 'result': 'ok', 'data': {'name': "Mosquitto Test"} }) + assert aioclient_mock.call_count == 0 + with patch('homeassistant.components.mqtt.' 'config_flow.FlowHandler.async_step_hassio', Mock(return_value=mock_coro({"type": "abort"}))) as mock_mqtt: @@ -44,6 +48,55 @@ async def test_hassio_discovery_startup(hass, aioclient_mock, hassio_client): }) +async def test_hassio_discovery_startup_done(hass, aioclient_mock, + hassio_client): + """Test startup and discovery with hass discovery.""" + aioclient_mock.get( + "http://127.0.0.1/discovery", json={ + 'result': 'ok', 'data': {'discovery': [ + { + "service": "mqtt", "uuid": "test", + "addon": "mosquitto", "config": + { + 'broker': 'mock-broker', + 'port': 1883, + 'username': 'mock-user', + 'password': 'mock-pass', + 'protocol': '3.1.1' + } + } + ]}}) + aioclient_mock.get( + "http://127.0.0.1/addons/mosquitto/info", json={ + 'result': 'ok', 'data': {'name': "Mosquitto Test"} + }) + + with patch('homeassistant.components.hassio.HassIO.update_hass_api', + Mock(return_value=mock_coro({"result": "ok"}))), \ + patch('homeassistant.components.hassio.HassIO.' + 'get_homeassistant_info', + Mock(side_effect=HassioAPIError())), \ + patch('homeassistant.components.mqtt.' + 'config_flow.FlowHandler.async_step_hassio', + Mock(return_value=mock_coro({"type": "abort"})) + ) as mock_mqtt: + await hass.async_start() + await async_setup_component(hass, 'hassio', { + 'http': { + 'api_password': API_PASSWORD + } + }) + await hass.async_block_till_done() + + assert aioclient_mock.call_count == 2 + assert mock_mqtt.called + mock_mqtt.assert_called_with({ + 'broker': 'mock-broker', 'port': 1883, 'username': 'mock-user', + 'password': 'mock-pass', 'protocol': '3.1.1', + 'addon': 'Mosquitto Test', + }) + + async def test_hassio_discovery_webhook(hass, aioclient_mock, hassio_client): """Test discovery webhook.""" aioclient_mock.get( From 1d78393680187e336dea5e64bc1fcd22dc030ef6 Mon Sep 17 00:00:00 2001 From: Paul Annekov Date: Wed, 10 Oct 2018 15:24:30 +0300 Subject: [PATCH 238/247] fixed 'on_startup() takes 0 positional arguments but 1 was given' (#17295) --- homeassistant/components/sensor/miflora.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/miflora.py b/homeassistant/components/sensor/miflora.py index a097974fbfd..74bb8261609 100644 --- a/homeassistant/components/sensor/miflora.py +++ b/homeassistant/components/sensor/miflora.py @@ -106,7 +106,7 @@ class MiFloraSensor(Entity): async def async_added_to_hass(self): """Set initial state.""" @callback - def on_startup(): + def on_startup(_): self.async_schedule_update_ha_state(True) self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, on_startup) From fa3f6ca2c749c0440e533774656fe51652658bce Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 10 Oct 2018 14:29:04 +0200 Subject: [PATCH 239/247] Bumped version to 0.80.0b5 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index e00abef3923..737a2c10cff 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 80 -PATCH_VERSION = '0b4' +PATCH_VERSION = '0b5' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From cfc5ebbfb06c2090a24ec855788f37a247079960 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 12 Oct 2018 14:57:29 +0200 Subject: [PATCH 240/247] Update frontend --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 27904faec1a..c06f659573e 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==20181007.0'] +REQUIREMENTS = ['home-assistant-frontend==20181012.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log', diff --git a/requirements_all.txt b/requirements_all.txt index 7da7f7e7847..c91b4127880 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -458,7 +458,7 @@ hole==0.3.0 holidays==0.9.7 # homeassistant.components.frontend -home-assistant-frontend==20181007.0 +home-assistant-frontend==20181012.0 # homeassistant.components.homekit_controller # homekit==0.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b4e995d580c..871714cc47d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -90,7 +90,7 @@ hdate==0.6.3 holidays==0.9.7 # homeassistant.components.frontend -home-assistant-frontend==20181007.0 +home-assistant-frontend==20181012.0 # homeassistant.components.homematicip_cloud homematicip==0.9.8 From f47e080f37c0cebaae391eee1cd64bc1fee2b8f2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 12 Oct 2018 14:58:09 +0200 Subject: [PATCH 241/247] Update translations --- .../components/auth/.translations/sv.json | 9 +++++-- .../deconz/.translations/zh-Hant.json | 2 +- .../components/hangouts/.translations/sv.json | 2 ++ .../components/ifttt/.translations/sv.json | 13 +++++++++ .../ifttt/.translations/zh-Hant.json | 4 +-- .../components/ios/.translations/sv.json | 3 +++ .../components/lifx/.translations/en.json | 15 +++++++++++ .../components/lifx/.translations/sv.json | 15 +++++++++++ .../lifx/.translations/zh-Hans.json | 15 +++++++++++ .../components/mqtt/.translations/sv.json | 22 +++++++++++++-- .../components/smhi/.translations/sv.json | 19 +++++++++++++ .../smhi/.translations/zh-Hans.json | 19 +++++++++++++ .../components/tradfri/.translations/sv.json | 5 +++- .../components/upnp/.translations/sv.json | 27 +++++++++++++++++++ .../components/zwave/.translations/ca.json | 7 ++++- .../components/zwave/.translations/en.json | 22 +++++++++++++++ .../components/zwave/.translations/sl.json | 22 +++++++++++++++ .../components/zwave/.translations/sv.json | 22 +++++++++++++++ .../zwave/.translations/zh-Hans.json | 22 +++++++++++++++ .../zwave/.translations/zh-Hant.json | 22 +++++++++++++++ 20 files changed, 278 insertions(+), 9 deletions(-) create mode 100644 homeassistant/components/lifx/.translations/en.json create mode 100644 homeassistant/components/lifx/.translations/sv.json create mode 100644 homeassistant/components/lifx/.translations/zh-Hans.json create mode 100644 homeassistant/components/smhi/.translations/sv.json create mode 100644 homeassistant/components/smhi/.translations/zh-Hans.json create mode 100644 homeassistant/components/upnp/.translations/sv.json create mode 100644 homeassistant/components/zwave/.translations/en.json create mode 100644 homeassistant/components/zwave/.translations/sl.json create mode 100644 homeassistant/components/zwave/.translations/sv.json create mode 100644 homeassistant/components/zwave/.translations/zh-Hans.json create mode 100644 homeassistant/components/zwave/.translations/zh-Hant.json diff --git a/homeassistant/components/auth/.translations/sv.json b/homeassistant/components/auth/.translations/sv.json index 604ae3c4fe5..9246a88c512 100644 --- a/homeassistant/components/auth/.translations/sv.json +++ b/homeassistant/components/auth/.translations/sv.json @@ -8,11 +8,16 @@ "invalid_code": "Ogiltig kod, var god f\u00f6rs\u00f6k igen." }, "step": { + "init": { + "description": "Var god v\u00e4lj en av notifieringstj\u00e4nsterna:", + "title": "Konfigurera ett eng\u00e5ngsl\u00f6senord levererat genom notifieringskomponenten" + }, "setup": { "description": "Ett eng\u00e5ngsl\u00f6senord har skickats av **notify.{notify_service}**. V\u00e4nligen ange det nedan:", - "title": "Verifiera installationen" + "title": "Verifiera inst\u00e4llningen" } - } + }, + "title": "Meddela eng\u00e5ngsl\u00f6senord" }, "totp": { "error": { diff --git a/homeassistant/components/deconz/.translations/zh-Hant.json b/homeassistant/components/deconz/.translations/zh-Hant.json index 5cd1a14d499..524f68d41bc 100644 --- a/homeassistant/components/deconz/.translations/zh-Hant.json +++ b/homeassistant/components/deconz/.translations/zh-Hant.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "Bridge \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "no_bridges": "\u672a\u641c\u5c0b\u5230 deCONZ Bridfe", - "one_instance_only": "\u7d44\u4ef6\u50c5\u652f\u63f4\u4e00\u7d44 deCONZ \u5be6\u4f8b" + "one_instance_only": "\u7d44\u4ef6\u50c5\u652f\u63f4\u4e00\u7d44 deCONZ \u7269\u4ef6" }, "error": { "no_key": "\u7121\u6cd5\u53d6\u5f97 API key" diff --git a/homeassistant/components/hangouts/.translations/sv.json b/homeassistant/components/hangouts/.translations/sv.json index 90bf4e97712..ae03fdbf722 100644 --- a/homeassistant/components/hangouts/.translations/sv.json +++ b/homeassistant/components/hangouts/.translations/sv.json @@ -14,6 +14,7 @@ "data": { "2fa": "2FA Pinkod" }, + "description": "Missing english translation", "title": "Tv\u00e5faktorsautentisering" }, "user": { @@ -21,6 +22,7 @@ "email": "E-postadress", "password": "L\u00f6senord" }, + "description": "Missing english translation", "title": "Google Hangouts-inloggning" } }, diff --git a/homeassistant/components/ifttt/.translations/sv.json b/homeassistant/components/ifttt/.translations/sv.json index 077956287b3..883bb042822 100644 --- a/homeassistant/components/ifttt/.translations/sv.json +++ b/homeassistant/components/ifttt/.translations/sv.json @@ -1,5 +1,18 @@ { "config": { + "abort": { + "not_internet_accessible": "Din Home Assistant instans m\u00e5ste vara tillg\u00e4nglig fr\u00e5n internet f\u00f6r att ta emot IFTTT meddelanden.", + "one_instance_allowed": "Endast en enda instans \u00e4r n\u00f6dv\u00e4ndig." + }, + "create_entry": { + "default": "F\u00f6r att skicka h\u00e4ndelser till Home Assistant m\u00e5ste du anv\u00e4nda \u00e5tg\u00e4rden \"G\u00f6r en webbf\u00f6rfr\u00e5gan\" fr\u00e5n [IFTTT Webhook applet] ( {applet_url} ).\n\n Fyll i f\u00f6ljande information:\n \n - URL: ` {webhook_url} `\n - Metod: POST\n - Inneh\u00e5llstyp: application / json\n\n Se [dokumentationen] ( {docs_url} ) om hur du konfigurerar automatiseringar f\u00f6r att hantera inkommande data." + }, + "step": { + "user": { + "description": "\u00c4r du s\u00e4ker p\u00e5 att du vill st\u00e4lla in IFTTT?", + "title": "St\u00e4lla in IFTTT Webhook Applet" + } + }, "title": "IFTTT" } } \ No newline at end of file diff --git a/homeassistant/components/ifttt/.translations/zh-Hant.json b/homeassistant/components/ifttt/.translations/zh-Hant.json index 5c75beddbe1..8610351f43b 100644 --- a/homeassistant/components/ifttt/.translations/zh-Hant.json +++ b/homeassistant/components/ifttt/.translations/zh-Hant.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "not_internet_accessible": "Home Assistant \u5be6\u4f8b\u5fc5\u9808\u80fd\u5920\u7531\u7db2\u969b\u7db2\u8def\u5b58\u53d6\uff0c\u65b9\u80fd\u63a5\u53d7 IFTTT \u8a0a\u606f\u3002", - "one_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u7d44\u5be6\u4f8b\u5373\u53ef\u3002" + "not_internet_accessible": "Home Assistant \u7269\u4ef6\u5fc5\u9808\u80fd\u5920\u7531\u7db2\u969b\u7db2\u8def\u5b58\u53d6\uff0c\u65b9\u80fd\u63a5\u53d7 IFTTT \u8a0a\u606f\u3002", + "one_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u7d44\u7269\u4ef6\u5373\u53ef\u3002" }, "create_entry": { "default": "\u6b32\u50b3\u9001\u4e8b\u4ef6\u81f3 Home Assistant\uff0c\u5c07\u9700\u8981\u7531 [IFTTT Webhook applet]({applet_url}) \u547c\u53eb\u300c\u9032\u884c Web \u8acb\u6c42\u300d\u52d5\u4f5c\u3002\n\n\u8acb\u586b\u5beb\u4e0b\u5217\u8cc7\u8a0a\uff1a\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\n\u95dc\u65bc\u5982\u4f55\u50b3\u5165\u8cc7\u6599\u81ea\u52d5\u5316\u8a2d\u5b9a\uff0c\u8acb\u53c3\u95b1[\u6587\u4ef6]({docs_url})\u4ee5\u9032\u884c\u4e86\u89e3\u3002" diff --git a/homeassistant/components/ios/.translations/sv.json b/homeassistant/components/ios/.translations/sv.json index 6806f9bab90..5a605ed8987 100644 --- a/homeassistant/components/ios/.translations/sv.json +++ b/homeassistant/components/ios/.translations/sv.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "single_instance_allowed": "Endast en enda konfiguration av Home Assistant iOS \u00e4r n\u00f6dv\u00e4ndig." + }, "step": { "confirm": { "description": "Vill du konfigurera Home Assistants iOS komponent?", diff --git a/homeassistant/components/lifx/.translations/en.json b/homeassistant/components/lifx/.translations/en.json new file mode 100644 index 00000000000..64fdc7516ea --- /dev/null +++ b/homeassistant/components/lifx/.translations/en.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "No LIFX devices found on the network.", + "single_instance_allowed": "Only a single configuration of LIFX is possible." + }, + "step": { + "confirm": { + "description": "Do you want to set up LIFX?", + "title": "LIFX" + } + }, + "title": "LIFX" + } +} \ No newline at end of file diff --git a/homeassistant/components/lifx/.translations/sv.json b/homeassistant/components/lifx/.translations/sv.json new file mode 100644 index 00000000000..a935e209bb4 --- /dev/null +++ b/homeassistant/components/lifx/.translations/sv.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Inga LIFX enheter hittas i n\u00e4tverket.", + "single_instance_allowed": "Endast en enda konfiguration av LIFX \u00e4r m\u00f6jlig." + }, + "step": { + "confirm": { + "description": "Vill du st\u00e4lla in LIFX?", + "title": "LIFX" + } + }, + "title": "LIFX" + } +} \ No newline at end of file diff --git a/homeassistant/components/lifx/.translations/zh-Hans.json b/homeassistant/components/lifx/.translations/zh-Hans.json new file mode 100644 index 00000000000..bc9375d807d --- /dev/null +++ b/homeassistant/components/lifx/.translations/zh-Hans.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u6ca1\u6709\u5728\u7f51\u7edc\u4e0a\u627e\u5230 LIFX \u8bbe\u5907\u3002", + "single_instance_allowed": "LIFX \u53ea\u80fd\u914d\u7f6e\u4e00\u6b21\u3002" + }, + "step": { + "confirm": { + "description": "\u60a8\u60f3\u8981\u914d\u7f6e LIFX \u5417\uff1f", + "title": "LIFX" + } + }, + "title": "LIFX" + } +} \ No newline at end of file diff --git a/homeassistant/components/mqtt/.translations/sv.json b/homeassistant/components/mqtt/.translations/sv.json index 7cf6d75b9c1..70e3720038d 100644 --- a/homeassistant/components/mqtt/.translations/sv.json +++ b/homeassistant/components/mqtt/.translations/sv.json @@ -1,13 +1,31 @@ { "config": { + "abort": { + "single_instance_allowed": "Endast en enda konfiguration av MQTT \u00e4r till\u00e5ten." + }, + "error": { + "cannot_connect": "Det gick inte att ansluta till broker." + }, "step": { "broker": { "data": { + "broker": "Broker", + "discovery": "Aktivera uppt\u00e4ckt", "password": "L\u00f6senord", "port": "Port", "username": "Anv\u00e4ndarnamn" - } + }, + "description": "V\u00e4nligen ange anslutningsinformationen f\u00f6r din MQTT broker.", + "title": "MQTT" + }, + "hassio_confirm": { + "data": { + "discovery": "Aktivera uppt\u00e4ckt" + }, + "description": "Vill du konfigurera Home Assistant f\u00f6r att ansluta till MQTT Broker som tillhandah\u00e5lls av hass.io-till\u00e4gget {addon} ?", + "title": "MQTT Broker via Hass.io till\u00e4gg" } - } + }, + "title": "MQTT" } } \ No newline at end of file diff --git a/homeassistant/components/smhi/.translations/sv.json b/homeassistant/components/smhi/.translations/sv.json new file mode 100644 index 00000000000..69073a0eb73 --- /dev/null +++ b/homeassistant/components/smhi/.translations/sv.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "name_exists": "Namnet finns redan", + "wrong_location": "Plats i Sverige endast" + }, + "step": { + "user": { + "data": { + "latitude": "Latitud", + "longitude": "Longitud", + "name": "Namn" + }, + "title": "Plats i Sverige" + } + }, + "title": "Svensk v\u00e4derservice (SMHI)" + } +} \ No newline at end of file diff --git a/homeassistant/components/smhi/.translations/zh-Hans.json b/homeassistant/components/smhi/.translations/zh-Hans.json new file mode 100644 index 00000000000..a70bb7a6722 --- /dev/null +++ b/homeassistant/components/smhi/.translations/zh-Hans.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "name_exists": "\u540d\u79f0\u5df2\u5b58\u5728", + "wrong_location": "\u4ec5\u9650\u745e\u5178\u7684\u4f4d\u7f6e" + }, + "step": { + "user": { + "data": { + "latitude": "\u7eac\u5ea6", + "longitude": "\u7ecf\u5ea6", + "name": "\u540d\u79f0" + }, + "title": "\u5728\u745e\u5178\u7684\u4f4d\u7f6e" + } + }, + "title": "\u745e\u5178\u6c14\u8c61\u670d\u52a1\uff08SMHI\uff09" + } +} \ No newline at end of file diff --git a/homeassistant/components/tradfri/.translations/sv.json b/homeassistant/components/tradfri/.translations/sv.json index ffe8bff22b4..34799050539 100644 --- a/homeassistant/components/tradfri/.translations/sv.json +++ b/homeassistant/components/tradfri/.translations/sv.json @@ -4,11 +4,14 @@ "already_configured": "Bryggan \u00e4r redan konfigurerad" }, "error": { - "cannot_connect": "Det gick inte att ansluta till gatewayen." + "cannot_connect": "Det gick inte att ansluta till gatewayen.", + "invalid_key": "Misslyckades med att registrera den angivna nyckeln. Om det h\u00e4r h\u00e4nder, f\u00f6rs\u00f6k starta om gatewayen igen.", + "timeout": "Timeout vid valididering av kod" }, "step": { "auth": { "data": { + "host": "V\u00e4rd", "security_code": "S\u00e4kerhetskod" }, "description": "Du kan hitta s\u00e4kerhetskoden p\u00e5 baksidan av din gateway.", diff --git a/homeassistant/components/upnp/.translations/sv.json b/homeassistant/components/upnp/.translations/sv.json new file mode 100644 index 00000000000..63c63781845 --- /dev/null +++ b/homeassistant/components/upnp/.translations/sv.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "UPnP/IGD \u00e4r redan konfigurerad", + "no_devices_discovered": "Inga UPnP/IGDs uppt\u00e4cktes", + "no_sensors_or_port_mapping": "Aktivera minst sensorer eller portmappning" + }, + "error": { + "one": "En", + "other": "Andra" + }, + "step": { + "init": { + "title": "UPnP/IGD" + }, + "user": { + "data": { + "enable_port_mapping": "Aktivera portmappning f\u00f6r Home Assistant", + "enable_sensors": "L\u00e4gg till trafiksensorer", + "igd": "UPnP/IGD" + }, + "title": "Konfigurationsalternativ f\u00f6r UPnP/IGD" + } + }, + "title": "UPnP/IGD" + } +} \ No newline at end of file diff --git a/homeassistant/components/zwave/.translations/ca.json b/homeassistant/components/zwave/.translations/ca.json index 8c39ca58201..b617a902374 100644 --- a/homeassistant/components/zwave/.translations/ca.json +++ b/homeassistant/components/zwave/.translations/ca.json @@ -4,11 +4,16 @@ "already_configured": "Z-Wave ja est\u00e0 configurat", "one_instance_only": "El component nom\u00e9s admet una inst\u00e0ncia de Z-Wave" }, + "error": { + "option_error": "Ha fallat la validaci\u00f3 de Z-Wave. \u00c9s correcta la ruta al port on hi ha la mem\u00f2ria USB?" + }, "step": { "user": { "data": { - "network_key": "Clau de xarxa (deixeu-ho en blanc per generar-la autom\u00e0ticament)" + "network_key": "Clau de xarxa (deixeu-ho en blanc per generar-la autom\u00e0ticament)", + "usb_path": "Ruta del port USB" }, + "description": "Consulteu https://www.home-assistant.io/docs/z-wave/installation/ per obtenir informaci\u00f3 sobre les variables de configuraci\u00f3", "title": "Configureu Z-Wave" } }, diff --git a/homeassistant/components/zwave/.translations/en.json b/homeassistant/components/zwave/.translations/en.json new file mode 100644 index 00000000000..081d5c858cb --- /dev/null +++ b/homeassistant/components/zwave/.translations/en.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Z-Wave is already configured", + "one_instance_only": "Component only supports one Z-Wave instance" + }, + "error": { + "option_error": "Z-Wave validation failed. Is the path to the USB stick correct?" + }, + "step": { + "user": { + "data": { + "network_key": "Network Key (leave blank to auto-generate)", + "usb_path": "USB Path" + }, + "description": "See https://www.home-assistant.io/docs/z-wave/installation/ for information on the configuration variables", + "title": "Set up Z-Wave" + } + }, + "title": "Z-Wave" + } +} \ No newline at end of file diff --git a/homeassistant/components/zwave/.translations/sl.json b/homeassistant/components/zwave/.translations/sl.json new file mode 100644 index 00000000000..fa799d1ed36 --- /dev/null +++ b/homeassistant/components/zwave/.translations/sl.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Z-Wave je \u017ee konfiguriran", + "one_instance_only": "Komponente podpirajo le eno Z-Wave instanco" + }, + "error": { + "option_error": "Potrjevanje Z-Wave ni uspelo. Ali je pot do USB klju\u010da pravilna?" + }, + "step": { + "user": { + "data": { + "network_key": "Omre\u017eni klju\u010d (pustite prazno za samodejno generiranje)", + "usb_path": "USB Pot" + }, + "description": "Za informacije o konfiguracijskih spremenljivka si oglejte https://www.home-assistant.io/docs/z-wave/installation/", + "title": "Nastavite Z-Wave" + } + }, + "title": "Z-Wave" + } +} \ No newline at end of file diff --git a/homeassistant/components/zwave/.translations/sv.json b/homeassistant/components/zwave/.translations/sv.json new file mode 100644 index 00000000000..508652a1784 --- /dev/null +++ b/homeassistant/components/zwave/.translations/sv.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Z-Wave \u00e4r redan konfigurerat", + "one_instance_only": "Komponenten st\u00f6der endast en Z-Wave-instans" + }, + "error": { + "option_error": "Z-Wave-valideringen misslyckades. \u00c4r s\u00f6kv\u00e4gen till USB-minnet korrekt?" + }, + "step": { + "user": { + "data": { + "network_key": "N\u00e4tverksnyckel (l\u00e4mna blank f\u00f6r automatisk generering)", + "usb_path": "USB-s\u00f6kv\u00e4g" + }, + "description": "Se https://www.home-assistant.io/docs/z-wave/installation/ f\u00f6r information om konfigurationsvariabler", + "title": "St\u00e4lla in Z-Wave" + } + }, + "title": "Z-Wave" + } +} \ No newline at end of file diff --git a/homeassistant/components/zwave/.translations/zh-Hans.json b/homeassistant/components/zwave/.translations/zh-Hans.json new file mode 100644 index 00000000000..2c72ce72c60 --- /dev/null +++ b/homeassistant/components/zwave/.translations/zh-Hans.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Z-Wave \u5df2\u914d\u7f6e\u5b8c\u6210", + "one_instance_only": "\u7ec4\u4ef6\u53ea\u652f\u6301\u4e00\u4e2a Z-Wave \u5b9e\u4f8b" + }, + "error": { + "option_error": "Z-Wave \u9a8c\u8bc1\u5931\u8d25\u3002 USB \u68d2\u7684\u8def\u5f84\u662f\u5426\u6b63\u786e\uff1f" + }, + "step": { + "user": { + "data": { + "network_key": "\u7f51\u7edc\u5bc6\u94a5\uff08\u7559\u7a7a\u5c06\u81ea\u52a8\u751f\u6210\uff09", + "usb_path": "USB \u8def\u5f84" + }, + "description": "\u6709\u5173\u914d\u7f6e\u7684\u4fe1\u606f\uff0c\u8bf7\u53c2\u9605 https://www.home-assistant.io/docs/z-wave/installation/", + "title": "\u8bbe\u7f6e Z-Wave" + } + }, + "title": "Z-Wave" + } +} \ No newline at end of file diff --git a/homeassistant/components/zwave/.translations/zh-Hant.json b/homeassistant/components/zwave/.translations/zh-Hant.json new file mode 100644 index 00000000000..2a84e8b3fd6 --- /dev/null +++ b/homeassistant/components/zwave/.translations/zh-Hant.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Z-Wave \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "one_instance_only": "\u7d44\u4ef6\u50c5\u652f\u63f4\u4e00\u7d44 Z-Wave \u7269\u4ef6" + }, + "error": { + "option_error": "Z-Wave \u9a57\u8b49\u5931\u6557\uff0c\u8acb\u78ba\u5b9a USB \u96a8\u8eab\u789f\u8def\u5f91\u6b63\u78ba\uff1f" + }, + "step": { + "user": { + "data": { + "network_key": "\u7db2\u8def\u5bc6\u9470\uff08\u4fdd\u7559\u7a7a\u767d\u5c07\u6703\u81ea\u52d5\u7522\u751f\uff09", + "usb_path": "USB \u8def\u5f91" + }, + "description": "\u95dc\u65bc\u8a2d\u5b9a\u8b8a\u6578\u8cc7\u8a0a\uff0c\u8acb\u53c3\u95b1 https://www.home-assistant.io/docs/z-wave/installation/", + "title": "\u8a2d\u5b9a Z-Wave" + } + }, + "title": "Z-Wave" + } +} \ No newline at end of file From d199f57aa8083ea3268f69245e6c41bd72060f91 Mon Sep 17 00:00:00 2001 From: Nikolay Vasilchuk Date: Thu, 11 Oct 2018 15:15:04 +0300 Subject: [PATCH 242/247] Logbook: filter by entity and period (#17095) * Filter logbook by entity_id * Filter logbook by period * Simple test * houndci-bot review * Tests * Test fix * Test Fix --- homeassistant/components/logbook.py | 19 ++++-- tests/components/test_logbook.py | 89 ++++++++++++++++++++++++++++- 2 files changed, 103 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/logbook.py b/homeassistant/components/logbook.py index 9e66c8d3aca..5cbd2b9432b 100644 --- a/homeassistant/components/logbook.py +++ b/homeassistant/components/logbook.py @@ -133,14 +133,21 @@ class LogbookView(HomeAssistantView): else: datetime = dt_util.start_of_local_day() - start_day = dt_util.as_utc(datetime) - end_day = start_day + timedelta(days=1) + period = request.query.get('period') + if period is None: + period = 1 + else: + period = int(period) + + entity_id = request.query.get('entity') + start_day = dt_util.as_utc(datetime) - timedelta(days=period - 1) + end_day = start_day + timedelta(days=period) hass = request.app['hass'] def json_events(): """Fetch events and generate JSON.""" return self.json(list( - _get_events(hass, self.config, start_day, end_day))) + _get_events(hass, self.config, start_day, end_day, entity_id))) return await hass.async_add_job(json_events) @@ -288,7 +295,7 @@ def humanify(hass, events): } -def _get_events(hass, config, start_day, end_day): +def _get_events(hass, config, start_day, end_day, entity_id=None): """Get events for a period of time.""" from homeassistant.components.recorder.models import Events, States from homeassistant.components.recorder.util import ( @@ -302,6 +309,10 @@ def _get_events(hass, config, start_day, end_day): & (Events.time_fired < end_day)) \ .filter((States.last_updated == States.last_changed) | (States.state_id.is_(None))) + + if entity_id is not None: + query = query.filter(States.entity_id == entity_id.lower()) + events = execute(query) return humanify(hass, _exclude_events(events, config)) diff --git a/tests/components/test_logbook.py b/tests/components/test_logbook.py index 8e7c2299731..3bb3ae57c68 100644 --- a/tests/components/test_logbook.py +++ b/tests/components/test_logbook.py @@ -1,7 +1,7 @@ """The tests for the logbook component.""" # pylint: disable=protected-access,invalid-name import logging -from datetime import timedelta +from datetime import (timedelta, datetime) import unittest from homeassistant.components import sun @@ -558,6 +558,93 @@ async def test_logbook_view(hass, aiohttp_client): assert response.status == 200 +async def test_logbook_view_period_entity(hass, aiohttp_client): + """Test the logbook view with period and entity.""" + await hass.async_add_job(init_recorder_component, hass) + await async_setup_component(hass, 'logbook', {}) + await hass.components.recorder.wait_connection_ready() + await hass.async_add_job(hass.data[recorder.DATA_INSTANCE].block_till_done) + + entity_id_test = 'switch.test' + hass.states.async_set(entity_id_test, STATE_OFF) + hass.states.async_set(entity_id_test, STATE_ON) + entity_id_second = 'switch.second' + hass.states.async_set(entity_id_second, STATE_OFF) + hass.states.async_set(entity_id_second, STATE_ON) + await hass.async_block_till_done() + await hass.async_add_job(hass.data[recorder.DATA_INSTANCE].block_till_done) + + client = await aiohttp_client(hass.http.app) + + # Today time 00:00:00 + start = dt_util.utcnow().date() + start_date = datetime(start.year, start.month, start.day) + + # Test today entries without filters + response = await client.get( + '/api/logbook/{}'.format(start_date.isoformat())) + assert response.status == 200 + json = await response.json() + assert len(json) == 2 + assert json[0]['entity_id'] == entity_id_test + assert json[1]['entity_id'] == entity_id_second + + # Test today entries with filter by period + response = await client.get( + '/api/logbook/{}?period=1'.format(start_date.isoformat())) + assert response.status == 200 + json = await response.json() + assert len(json) == 2 + assert json[0]['entity_id'] == entity_id_test + assert json[1]['entity_id'] == entity_id_second + + # Test today entries with filter by entity_id + response = await client.get( + '/api/logbook/{}?entity=switch.test'.format( + start_date.isoformat())) + assert response.status == 200 + json = await response.json() + assert len(json) == 1 + assert json[0]['entity_id'] == entity_id_test + + # Test entries for 3 days with filter by entity_id + response = await client.get( + '/api/logbook/{}?period=3&entity=switch.test'.format( + start_date.isoformat())) + assert response.status == 200 + json = await response.json() + assert len(json) == 1 + assert json[0]['entity_id'] == entity_id_test + + # Tomorrow time 00:00:00 + start = (dt_util.utcnow() + timedelta(days=1)).date() + start_date = datetime(start.year, start.month, start.day) + + # Test tomorrow entries without filters + response = await client.get( + '/api/logbook/{}'.format(start_date.isoformat())) + assert response.status == 200 + json = await response.json() + assert len(json) == 0 + + # Test tomorrow entries with filter by entity_id + response = await client.get( + '/api/logbook/{}?entity=switch.test'.format( + start_date.isoformat())) + assert response.status == 200 + json = await response.json() + assert len(json) == 0 + + # Test entries from tomorrow to 3 days ago with filter by entity_id + response = await client.get( + '/api/logbook/{}?period=3&entity=switch.test'.format( + start_date.isoformat())) + assert response.status == 200 + json = await response.json() + assert len(json) == 1 + assert json[0]['entity_id'] == entity_id_test + + async def test_humanify_alexa_event(hass): """Test humanifying Alexa event.""" hass.states.async_set('light.kitchen', 'on', { From 05ae8f9dd4bea024a7c079c9d8751a3106ee283e Mon Sep 17 00:00:00 2001 From: Alok Saboo Date: Tue, 9 Oct 2018 15:43:59 -0400 Subject: [PATCH 243/247] Fix samsung bug (#17285) --- homeassistant/components/media_player/samsungtv.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/media_player/samsungtv.py b/homeassistant/components/media_player/samsungtv.py index 3573b28b4e3..3a66aa66dc0 100644 --- a/homeassistant/components/media_player/samsungtv.py +++ b/homeassistant/components/media_player/samsungtv.py @@ -30,7 +30,7 @@ _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = 'Samsung TV Remote' DEFAULT_PORT = 55000 -DEFAULT_TIMEOUT = 0 +DEFAULT_TIMEOUT = 1 KEY_PRESS_TIMEOUT = 1.2 KNOWN_DEVICES_KEY = 'samsungtv_known_devices' From 8778b707f14edbd7aec479286b0636b2c5e1cacb Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Thu, 11 Oct 2018 10:37:34 +0200 Subject: [PATCH 244/247] Allow tradfri groups for new imported entries (#17310) * Clean up leftover config schema option * Allow import groups via new config yaml setup * Fix and add test * Add a test without groups for legacy import * Change default import groups to False * Fix I/O in test --- homeassistant/components/tradfri/__init__.py | 7 +- .../components/tradfri/config_flow.py | 5 +- tests/components/tradfri/conftest.py | 12 +++ tests/components/tradfri/test_config_flow.py | 96 +++++++++++++++---- tests/components/tradfri/test_init.py | 2 +- 5 files changed, 96 insertions(+), 26 deletions(-) create mode 100644 tests/components/tradfri/conftest.py diff --git a/homeassistant/components/tradfri/__init__.py b/homeassistant/components/tradfri/__init__.py index e9ee8d0898b..ba13b8d511a 100644 --- a/homeassistant/components/tradfri/__init__.py +++ b/homeassistant/components/tradfri/__init__.py @@ -25,11 +25,11 @@ CONFIG_FILE = '.tradfri_psk.conf' KEY_GATEWAY = 'tradfri_gateway' KEY_API = 'tradfri_api' CONF_ALLOW_TRADFRI_GROUPS = 'allow_tradfri_groups' -DEFAULT_ALLOW_TRADFRI_GROUPS = True +DEFAULT_ALLOW_TRADFRI_GROUPS = False CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ - vol.Inclusive(CONF_HOST, 'gateway'): cv.string, + vol.Optional(CONF_HOST): cv.string, vol.Optional(CONF_ALLOW_TRADFRI_GROUPS, default=DEFAULT_ALLOW_TRADFRI_GROUPS): cv.boolean, }) @@ -64,13 +64,14 @@ async def async_setup(hass, config): )) host = conf.get(CONF_HOST) + import_groups = conf[CONF_ALLOW_TRADFRI_GROUPS] if host is None or host in configured_hosts or host in legacy_hosts: return True hass.async_create_task(hass.config_entries.flow.async_init( DOMAIN, context={'source': config_entries.SOURCE_IMPORT}, - data={'host': host} + data={CONF_HOST: host, CONF_IMPORT_GROUPS: import_groups} )) return True diff --git a/homeassistant/components/tradfri/config_flow.py b/homeassistant/components/tradfri/config_flow.py index a3452e50c4d..2e24fde8294 100644 --- a/homeassistant/components/tradfri/config_flow.py +++ b/homeassistant/components/tradfri/config_flow.py @@ -34,6 +34,7 @@ class FlowHandler(config_entries.ConfigFlow): def __init__(self): """Initialize flow.""" self._host = None + self._import_groups = False async def async_step_user(self, user_input=None): """Handle a flow initialized by the user.""" @@ -52,7 +53,8 @@ class FlowHandler(config_entries.ConfigFlow): # We don't ask for import group anymore as group state # is not reliable, don't want to show that to the user. - auth[CONF_IMPORT_GROUPS] = False + # But we still allow specifying import group via config yaml. + auth[CONF_IMPORT_GROUPS] = self._import_groups return await self._entry_from_data(auth) @@ -97,6 +99,7 @@ class FlowHandler(config_entries.ConfigFlow): # Happens if user has host directly in configuration.yaml if 'key' not in user_input: self._host = user_input['host'] + self._import_groups = user_input[CONF_IMPORT_GROUPS] return await self.async_step_auth() try: diff --git a/tests/components/tradfri/conftest.py b/tests/components/tradfri/conftest.py new file mode 100644 index 00000000000..9a5745264b7 --- /dev/null +++ b/tests/components/tradfri/conftest.py @@ -0,0 +1,12 @@ +"""Common tradfri test fixtures.""" +from unittest.mock import patch + +import pytest + + +@pytest.fixture +def mock_gateway_info(): + """Mock get_gateway_info.""" + with patch('homeassistant.components.tradfri.config_flow.' + 'get_gateway_info') as mock_gateway: + yield mock_gateway diff --git a/tests/components/tradfri/test_config_flow.py b/tests/components/tradfri/test_config_flow.py index 99566356f61..6756a01bbc7 100644 --- a/tests/components/tradfri/test_config_flow.py +++ b/tests/components/tradfri/test_config_flow.py @@ -17,14 +17,6 @@ def mock_auth(): yield mock_auth -@pytest.fixture -def mock_gateway_info(): - """Mock get_gateway_info.""" - with patch('homeassistant.components.tradfri.config_flow.' - 'get_gateway_info') as mock_gateway: - yield mock_gateway - - @pytest.fixture def mock_entry_setup(): """Mock entry setup.""" @@ -125,34 +117,65 @@ async def test_discovery_connection(hass, mock_auth, mock_entry_setup): } -async def test_import_connection(hass, mock_gateway_info, mock_entry_setup): +async def test_import_connection(hass, mock_auth, mock_entry_setup): """Test a connection via import.""" - mock_gateway_info.side_effect = \ - lambda hass, host, identity, key: mock_coro({ - 'host': host, - 'identity': identity, - 'key': key, - 'gateway_id': 'mock-gateway' - }) + mock_auth.side_effect = lambda hass, host, code: mock_coro({ + 'host': host, + 'gateway_id': 'bla', + 'identity': 'mock-iden', + 'key': 'mock-key', + }) - result = await hass.config_entries.flow.async_init( + flow = await hass.config_entries.flow.async_init( 'tradfri', context={'source': 'import'}, data={ 'host': '123.123.123.123', - 'identity': 'mock-iden', - 'key': 'mock-key', 'import_groups': True }) + result = await hass.config_entries.flow.async_configure(flow['flow_id'], { + 'security_code': 'abcd', + }) + assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result['result'].data == { 'host': '123.123.123.123', - 'gateway_id': 'mock-gateway', + 'gateway_id': 'bla', 'identity': 'mock-iden', 'key': 'mock-key', 'import_groups': True } - assert len(mock_gateway_info.mock_calls) == 1 + assert len(mock_entry_setup.mock_calls) == 1 + + +async def test_import_connection_no_groups(hass, mock_auth, mock_entry_setup): + """Test a connection via import and no groups allowed.""" + mock_auth.side_effect = lambda hass, host, code: mock_coro({ + 'host': host, + 'gateway_id': 'bla', + 'identity': 'mock-iden', + 'key': 'mock-key', + }) + + flow = await hass.config_entries.flow.async_init( + 'tradfri', context={'source': 'import'}, data={ + 'host': '123.123.123.123', + 'import_groups': False + }) + + result = await hass.config_entries.flow.async_configure(flow['flow_id'], { + 'security_code': 'abcd', + }) + + assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result['result'].data == { + 'host': '123.123.123.123', + 'gateway_id': 'bla', + 'identity': 'mock-iden', + 'key': 'mock-key', + 'import_groups': False + } + assert len(mock_entry_setup.mock_calls) == 1 @@ -187,6 +210,37 @@ async def test_import_connection_legacy(hass, mock_gateway_info, assert len(mock_entry_setup.mock_calls) == 1 +async def test_import_connection_legacy_no_groups( + hass, mock_gateway_info, mock_entry_setup): + """Test a connection via legacy import and no groups allowed.""" + mock_gateway_info.side_effect = \ + lambda hass, host, identity, key: mock_coro({ + 'host': host, + 'identity': identity, + 'key': key, + 'gateway_id': 'mock-gateway' + }) + + result = await hass.config_entries.flow.async_init( + 'tradfri', context={'source': 'import'}, data={ + 'host': '123.123.123.123', + 'key': 'mock-key', + 'import_groups': False + }) + + assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result['result'].data == { + 'host': '123.123.123.123', + 'gateway_id': 'mock-gateway', + 'identity': 'homeassistant', + 'key': 'mock-key', + 'import_groups': False + } + + assert len(mock_gateway_info.mock_calls) == 1 + assert len(mock_entry_setup.mock_calls) == 1 + + async def test_discovery_duplicate_aborted(hass): """Test a duplicate discovery host is ignored.""" MockConfigEntry( diff --git a/tests/components/tradfri/test_init.py b/tests/components/tradfri/test_init.py index 4527e87f605..800c7b72ee6 100644 --- a/tests/components/tradfri/test_init.py +++ b/tests/components/tradfri/test_init.py @@ -58,7 +58,7 @@ async def test_config_json_host_not_imported(hass): assert len(mock_init.mock_calls) == 0 -async def test_config_json_host_imported(hass): +async def test_config_json_host_imported(hass, mock_gateway_info): """Test that we import a configured host.""" with patch('homeassistant.components.tradfri.load_json', return_value={'mock-host': {'key': 'some-info'}}): From bc036cc2fe52e4102e2ec6b76f81932a3ecb1234 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Thu, 11 Oct 2018 01:02:00 +0200 Subject: [PATCH 245/247] Fix auth for hass.io (#17318) * Update test_auth.py * Update auth.py * Update test_auth.py --- homeassistant/components/hassio/auth.py | 5 ++--- tests/components/hassio/test_auth.py | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/hassio/auth.py b/homeassistant/components/hassio/auth.py index 73ef5aa29cc..951110271d4 100644 --- a/homeassistant/components/hassio/auth.py +++ b/homeassistant/components/hassio/auth.py @@ -4,8 +4,7 @@ from ipaddress import ip_address import os from aiohttp import web -from aiohttp.web_exceptions import ( - HTTPForbidden, HTTPNotFound, HTTPUnauthorized) +from aiohttp.web_exceptions import HTTPForbidden, HTTPNotFound import voluptuous as vol from homeassistant.core import callback @@ -72,4 +71,4 @@ class HassIOAuth(HomeAssistantView): try: await provider.async_validate_login(username, password) except HomeAssistantError: - raise HTTPUnauthorized() from None + raise HTTPForbidden() from None diff --git a/tests/components/hassio/test_auth.py b/tests/components/hassio/test_auth.py index 50d9488a19c..b3a6ae223f9 100644 --- a/tests/components/hassio/test_auth.py +++ b/tests/components/hassio/test_auth.py @@ -50,7 +50,7 @@ async def test_login_error(hass, hassio_client): ) # Check we got right response - assert resp.status == 401 + assert resp.status == 403 mock_login.assert_called_with("test", "123456") From edb3722abdd5d149013e74d0f0707993fcdd3824 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Thu, 11 Oct 2018 10:55:38 +0200 Subject: [PATCH 246/247] Hass.io auth/sso part2 (#17324) * Update discovery.py * Create const.py * Update auth.py * Update const.py * Update const.py * Update http.py * Update handler.py * Update auth.py * Update auth.py * Update test_auth.py --- homeassistant/components/hassio/auth.py | 8 ++--- homeassistant/components/hassio/const.py | 12 +++++++ homeassistant/components/hassio/discovery.py | 10 ++---- homeassistant/components/hassio/handler.py | 4 +-- homeassistant/components/hassio/http.py | 3 +- tests/components/hassio/test_auth.py | 34 ++++++++++++++++++-- 6 files changed, 54 insertions(+), 17 deletions(-) create mode 100644 homeassistant/components/hassio/const.py diff --git a/homeassistant/components/hassio/auth.py b/homeassistant/components/hassio/auth.py index 951110271d4..4be3ba9956c 100644 --- a/homeassistant/components/hassio/auth.py +++ b/homeassistant/components/hassio/auth.py @@ -14,16 +14,16 @@ from homeassistant.components.http import HomeAssistantView from homeassistant.components.http.const import KEY_REAL_IP from homeassistant.components.http.data_validator import RequestDataValidator -_LOGGER = logging.getLogger(__name__) +from .const import ATTR_USERNAME, ATTR_PASSWORD, ATTR_ADDON -ATTR_USERNAME = 'username' -ATTR_PASSWORD = 'password' +_LOGGER = logging.getLogger(__name__) SCHEMA_API_AUTH = vol.Schema({ vol.Required(ATTR_USERNAME): cv.string, vol.Required(ATTR_PASSWORD): cv.string, -}) + vol.Required(ATTR_ADDON): cv.string, +}, extra=vol.ALLOW_EXTRA) @callback diff --git a/homeassistant/components/hassio/const.py b/homeassistant/components/hassio/const.py new file mode 100644 index 00000000000..c539169ebe3 --- /dev/null +++ b/homeassistant/components/hassio/const.py @@ -0,0 +1,12 @@ +"""Hass.io const variables.""" + +ATTR_DISCOVERY = 'discovery' +ATTR_ADDON = 'addon' +ATTR_NAME = 'name' +ATTR_SERVICE = 'service' +ATTR_CONFIG = 'config' +ATTR_UUID = 'uuid' +ATTR_USERNAME = 'username' +ATTR_PASSWORD = 'password' + +X_HASSIO = 'X-HASSIO-KEY' diff --git a/homeassistant/components/hassio/discovery.py b/homeassistant/components/hassio/discovery.py index 3c5242607c1..4c7c5a6597f 100644 --- a/homeassistant/components/hassio/discovery.py +++ b/homeassistant/components/hassio/discovery.py @@ -10,16 +10,12 @@ from homeassistant.const import EVENT_HOMEASSISTANT_START from homeassistant.components.http import HomeAssistantView from .handler import HassioAPIError +from .const import ( + ATTR_DISCOVERY, ATTR_ADDON, ATTR_NAME, ATTR_SERVICE, ATTR_CONFIG, + ATTR_UUID) _LOGGER = logging.getLogger(__name__) -ATTR_DISCOVERY = 'discovery' -ATTR_ADDON = 'addon' -ATTR_NAME = 'name' -ATTR_SERVICE = 'service' -ATTR_CONFIG = 'config' -ATTR_UUID = 'uuid' - @callback def async_setup_discovery(hass, hassio, config): diff --git a/homeassistant/components/hassio/handler.py b/homeassistant/components/hassio/handler.py index 7c450b49bcc..91019776eeb 100644 --- a/homeassistant/components/hassio/handler.py +++ b/homeassistant/components/hassio/handler.py @@ -16,9 +16,9 @@ from homeassistant.components.http import ( CONF_SSL_CERTIFICATE) from homeassistant.const import CONF_TIME_ZONE, SERVER_PORT -_LOGGER = logging.getLogger(__name__) +from .const import X_HASSIO -X_HASSIO = 'X-HASSIO-KEY' +_LOGGER = logging.getLogger(__name__) class HassioAPIError(RuntimeError): diff --git a/homeassistant/components/hassio/http.py b/homeassistant/components/hassio/http.py index abd1c16ba8b..c3bd18fa9bb 100644 --- a/homeassistant/components/hassio/http.py +++ b/homeassistant/components/hassio/http.py @@ -18,9 +18,10 @@ from aiohttp.web_exceptions import HTTPBadGateway from homeassistant.const import CONTENT_TYPE_TEXT_PLAIN from homeassistant.components.http import KEY_AUTHENTICATED, HomeAssistantView +from .const import X_HASSIO + _LOGGER = logging.getLogger(__name__) -X_HASSIO = 'X-HASSIO-KEY' NO_TIMEOUT = re.compile( r'^(?:' diff --git a/tests/components/hassio/test_auth.py b/tests/components/hassio/test_auth.py index b3a6ae223f9..fdf3230dedc 100644 --- a/tests/components/hassio/test_auth.py +++ b/tests/components/hassio/test_auth.py @@ -19,7 +19,8 @@ async def test_login_success(hass, hassio_client): '/api/hassio_auth', json={ "username": "test", - "password": "123456" + "password": "123456", + "addon": "samba", }, headers={ HTTP_HEADER_HA_AUTH: API_PASSWORD @@ -42,7 +43,8 @@ async def test_login_error(hass, hassio_client): '/api/hassio_auth', json={ "username": "test", - "password": "123456" + "password": "123456", + "addon": "samba", }, headers={ HTTP_HEADER_HA_AUTH: API_PASSWORD @@ -83,7 +85,8 @@ async def test_login_no_username(hass, hassio_client): resp = await hassio_client.post( '/api/hassio_auth', json={ - "password": "123456" + "password": "123456", + "addon": "samba", }, headers={ HTTP_HEADER_HA_AUTH: API_PASSWORD @@ -93,3 +96,28 @@ async def test_login_no_username(hass, hassio_client): # Check we got right response assert resp.status == 400 assert not mock_login.called + + +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: + resp = await hassio_client.post( + '/api/hassio_auth', + json={ + "username": "test", + "password": "123456", + "addon": "samba", + "path": "/share", + }, + headers={ + HTTP_HEADER_HA_AUTH: API_PASSWORD + } + ) + + # Check we got right response + assert resp.status == 200 + mock_login.assert_called_with("test", "123456") From cd29b47924cf92c6fc760b8dd965647a634a8c6a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 12 Oct 2018 14:59:36 +0200 Subject: [PATCH 247/247] Bumped version to 0.80.0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 737a2c10cff..d023591c828 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 80 -PATCH_VERSION = '0b5' +PATCH_VERSION = '0' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3)