From cf87b76b0c5b7df729e8331e2d01cf20c0902c83 Mon Sep 17 00:00:00 2001 From: superpuffin <35958013+superpuffin@users.noreply.github.com> Date: Mon, 30 Jul 2018 16:15:13 +0200 Subject: [PATCH 001/117] Upgrade Adafruit-DHT to 1.3.3 (#15706) * Change to newer pip package The package Adafruit_Python_DHT==1.3.2 was broken and would not install, breaking DHT sensor support in Home assistant. It has since been fixed in Adafruit-DHT==1.3.3. See: https://github.com/adafruit/Adafruit_Python_DHT/issues/99 * Update requirements_all.txt New or updated dependencies have been added to `requirements_all.txt` by running `script/gen_requirements_all.py`. * Comment out Adafruit-DHT Adafruit_Python_DHT changed name to Adafruit-DHT, which still need pyx support breaking our CI, need to be comment out. * Update requirements_all.txt --- homeassistant/components/sensor/dht.py | 2 +- requirements_all.txt | 6 +++--- script/gen_requirements_all.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/sensor/dht.py b/homeassistant/components/sensor/dht.py index 6770594b919961..e3aaf2f84840fd 100644 --- a/homeassistant/components/sensor/dht.py +++ b/homeassistant/components/sensor/dht.py @@ -17,7 +17,7 @@ from homeassistant.util import Throttle from homeassistant.util.temperature import celsius_to_fahrenheit -REQUIREMENTS = ['Adafruit_Python_DHT==1.3.2'] +REQUIREMENTS = ['Adafruit-DHT==1.3.3'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 9be2d66f327e0f..eda2b604115756 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -14,6 +14,9 @@ voluptuous==0.11.3 # homeassistant.components.nuimo_controller --only-binary=all nuimo==0.1.0 +# homeassistant.components.sensor.dht +# Adafruit-DHT==1.3.3 + # homeassistant.components.sensor.sht31 Adafruit-GPIO==1.0.3 @@ -23,9 +26,6 @@ Adafruit-SHT31==1.0.2 # homeassistant.components.bbb_gpio # Adafruit_BBIO==1.0.0 -# homeassistant.components.sensor.dht -# Adafruit_Python_DHT==1.3.2 - # homeassistant.components.doorbird DoorBirdPy==0.1.3 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index d92502de0782f4..28c96e737ff171 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -11,7 +11,7 @@ 'RPi.GPIO', 'raspihats', 'rpi-rf', - 'Adafruit_Python_DHT', + 'Adafruit-DHT', 'Adafruit_BBIO', 'fritzconnection', 'pybluez', From 3208ad27ace4e5d5b53a1dc0950363bc09a5e773 Mon Sep 17 00:00:00 2001 From: Teemu R Date: Mon, 30 Jul 2018 17:09:38 +0200 Subject: [PATCH 002/117] Add kodi unique id based on discovery (#15093) * kodi add unique id based on discovery * initialize unique_id to None * use netdisco-extracted mac_address * use an uuid instead of mac for real uniqueness * add missing docstring * verify that there is no entity already for the given unique id * whitespace fix --- homeassistant/components/media_player/kodi.py | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/media_player/kodi.py b/homeassistant/components/media_player/kodi.py index 8758e969db14e3..08de2d00835303 100644 --- a/homeassistant/components/media_player/kodi.py +++ b/homeassistant/components/media_player/kodi.py @@ -160,6 +160,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): if DATA_KODI not in hass.data: hass.data[DATA_KODI] = dict() + unique_id = None # Is this a manual configuration? if discovery_info is None: name = config.get(CONF_NAME) @@ -175,6 +176,9 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): tcp_port = DEFAULT_TCP_PORT encryption = DEFAULT_PROXY_SSL websocket = DEFAULT_ENABLE_WEBSOCKET + properties = discovery_info.get('properties') + if properties is not None: + unique_id = properties.get('uuid', None) # Only add a device once, so discovered devices do not override manual # config. @@ -182,6 +186,14 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): if ip_addr in hass.data[DATA_KODI]: return + # If we got an unique id, check that it does not exist already. + # This is necessary as netdisco does not deterministally return the same + # advertisement when the service is offered over multiple IP addresses. + if unique_id is not None: + for device in hass.data[DATA_KODI].values(): + if device.unique_id == unique_id: + return + entity = KodiDevice( hass, name=name, @@ -190,7 +202,8 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): password=config.get(CONF_PASSWORD), turn_on_action=config.get(CONF_TURN_ON_ACTION), turn_off_action=config.get(CONF_TURN_OFF_ACTION), - timeout=config.get(CONF_TIMEOUT), websocket=websocket) + timeout=config.get(CONF_TIMEOUT), websocket=websocket, + unique_id=unique_id) hass.data[DATA_KODI][ip_addr] = entity async_add_devices([entity], update_before_add=True) @@ -260,12 +273,14 @@ class KodiDevice(MediaPlayerDevice): def __init__(self, hass, name, host, port, tcp_port, encryption=False, username=None, password=None, turn_on_action=None, turn_off_action=None, - timeout=DEFAULT_TIMEOUT, websocket=True): + timeout=DEFAULT_TIMEOUT, websocket=True, + unique_id=None): """Initialize the Kodi device.""" import jsonrpc_async import jsonrpc_websocket self.hass = hass self._name = name + self._unique_id = unique_id kwargs = { 'timeout': timeout, @@ -384,6 +399,11 @@ def _get_players(self): _LOGGER.debug("Unable to fetch kodi data", exc_info=True) return None + @property + def unique_id(self): + """Return the unique id of the device.""" + return self._unique_id + @property def state(self): """Return the state of the device.""" From 1b2d0e7a6ff107ef3886a747a403b9aff965d743 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Mon, 30 Jul 2018 22:56:52 -0600 Subject: [PATCH 003/117] Better handling of Yi camera being disconnected (#15754) * Better handling of Yi camera being disconnected * Handling video processing as well * Cleanup * Member-requested changes * Member-requested changes --- homeassistant/components/camera/yi.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/camera/yi.py b/homeassistant/components/camera/yi.py index b575a705f98bdf..4efc2c7d8ba9dc 100644 --- a/homeassistant/components/camera/yi.py +++ b/homeassistant/components/camera/yi.py @@ -57,6 +57,7 @@ def __init__(self, hass, config): self._last_url = None self._manager = hass.data[DATA_FFMPEG] self._name = config[CONF_NAME] + self._is_on = True self.host = config[CONF_HOST] self.port = config[CONF_PORT] self.path = config[CONF_PATH] @@ -68,6 +69,11 @@ def brand(self): """Camera brand.""" return DEFAULT_BRAND + @property + def is_on(self): + """Determine whether the camera is on.""" + return self._is_on + @property def name(self): """Return the name of this camera.""" @@ -81,7 +87,7 @@ async def _get_latest_video_url(self): try: await ftp.connect(self.host) await ftp.login(self.user, self.passwd) - except StatusCodeError as err: + except (ConnectionRefusedError, StatusCodeError) as err: raise PlatformNotReady(err) try: @@ -101,12 +107,13 @@ async def _get_latest_video_url(self): return None await ftp.quit() - + self._is_on = True return 'ftp://{0}:{1}@{2}:{3}{4}/{5}/{6}'.format( self.user, self.passwd, self.host, self.port, self.path, latest_dir, videos[-1]) except (ConnectionRefusedError, StatusCodeError) as err: _LOGGER.error('Error while fetching video: %s', err) + self._is_on = False return None async def async_camera_image(self): @@ -114,7 +121,7 @@ async def async_camera_image(self): from haffmpeg import ImageFrame, IMAGE_JPEG url = await self._get_latest_video_url() - if url != self._last_url: + if url and url != self._last_url: ffmpeg = ImageFrame(self._manager.binary, loop=self.hass.loop) self._last_image = await asyncio.shield( ffmpeg.get_image( @@ -130,6 +137,9 @@ async def handle_async_mjpeg_stream(self, request): """Generate an HTTP MJPEG stream from the camera.""" from haffmpeg import CameraMjpeg + if not self._is_on: + return + stream = CameraMjpeg(self._manager.binary, loop=self.hass.loop) await stream.open_camera( self._last_url, extra_cmd=self._extra_arguments) From eeb79476debf6b6338516daa661bf2d292f9eae4 Mon Sep 17 00:00:00 2001 From: Jason Hu Date: Mon, 30 Jul 2018 21:59:18 -0700 Subject: [PATCH 004/117] Decouple login flow view and data entry flow view (#15715) --- homeassistant/components/auth/login_flow.py | 64 ++++++++++++++++++--- 1 file changed, 55 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/auth/login_flow.py b/homeassistant/components/auth/login_flow.py index 6d1b6cf4ecfd62..bced421d6f982b 100644 --- a/homeassistant/components/auth/login_flow.py +++ b/homeassistant/components/auth/login_flow.py @@ -68,8 +68,6 @@ log_invalid_auth from homeassistant.components.http.data_validator import RequestDataValidator from homeassistant.components.http.view import HomeAssistantView -from homeassistant.helpers.data_entry_flow import ( - FlowManagerIndexView, FlowManagerResourceView) from . import indieauth @@ -97,13 +95,41 @@ async def get(self, request): } for provider in request.app['hass'].auth.auth_providers]) -class LoginFlowIndexView(FlowManagerIndexView): +def _prepare_result_json(result): + """Convert result to JSON.""" + if result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY: + data = result.copy() + data.pop('result') + data.pop('data') + return data + + if result['type'] != data_entry_flow.RESULT_TYPE_FORM: + return result + + import voluptuous_serialize + + data = result.copy() + + schema = data['data_schema'] + if schema is None: + data['data_schema'] = [] + else: + data['data_schema'] = voluptuous_serialize.convert(schema) + + return data + + +class LoginFlowIndexView(HomeAssistantView): """View to create a config flow.""" url = '/auth/login_flow' name = 'api:auth:login_flow' requires_auth = False + def __init__(self, flow_mgr): + """Initialize the flow manager index view.""" + self._flow_mgr = flow_mgr + async def get(self, request): """Do not allow index of flows in progress.""" return aiohttp.web.Response(status=405) @@ -120,11 +146,22 @@ async def post(self, request, data): data['redirect_uri']): return self.json_message('invalid client id or redirect uri', 400) - # pylint: disable=no-value-for-parameter - return await super().post(request) + if isinstance(data['handler'], list): + handler = tuple(data['handler']) + else: + handler = data['handler'] + try: + result = await self._flow_mgr.async_init(handler) + except data_entry_flow.UnknownHandler: + return self.json_message('Invalid handler specified', 404) + except data_entry_flow.UnknownStep: + return self.json_message('Handler does not support init', 400) -class LoginFlowResourceView(FlowManagerResourceView): + return self.json(_prepare_result_json(result)) + + +class LoginFlowResourceView(HomeAssistantView): """View to interact with the flow manager.""" url = '/auth/login_flow/{flow_id}' @@ -133,10 +170,10 @@ class LoginFlowResourceView(FlowManagerResourceView): def __init__(self, flow_mgr, store_credentials): """Initialize the login flow resource view.""" - super().__init__(flow_mgr) + self._flow_mgr = flow_mgr self._store_credentials = store_credentials - async def get(self, request, flow_id): + async def get(self, request): """Do not allow getting status of a flow in progress.""" return self.json_message('Invalid flow specified', 404) @@ -164,9 +201,18 @@ async def post(self, request, flow_id, data): if result['errors'] is not None and \ result['errors'].get('base') == 'invalid_auth': await process_wrong_login(request) - return self.json(self._prepare_result_json(result)) + return self.json(_prepare_result_json(result)) result.pop('data') result['result'] = self._store_credentials(client_id, result['result']) return self.json(result) + + async def delete(self, request, flow_id): + """Cancel a flow in progress.""" + try: + self._flow_mgr.async_abort(flow_id) + except data_entry_flow.UnknownFlow: + return self.json_message('Invalid flow specified', 404) + + return self.json_message('Flow aborted') From 951372491c160e0fa766afff175be7609e0510f6 Mon Sep 17 00:00:00 2001 From: Andrey Kupreychik Date: Tue, 31 Jul 2018 16:14:49 +0700 Subject: [PATCH 005/117] Fixed NDMS for latest firmware (#15511) * Fixed NDMS for latest firmware. Now using telnet instead of Web Interface * Using external library for NDMS interactions * updated requirements_all * renamed `mac` to `device` back * Using generators for name and attributes fetching --- .../device_tracker/keenetic_ndms2.py | 88 ++++++++----------- requirements_all.txt | 3 + 2 files changed, 38 insertions(+), 53 deletions(-) diff --git a/homeassistant/components/device_tracker/keenetic_ndms2.py b/homeassistant/components/device_tracker/keenetic_ndms2.py index 36dc1182a9294e..4b5e3d6333d909 100644 --- a/homeassistant/components/device_tracker/keenetic_ndms2.py +++ b/homeassistant/components/device_tracker/keenetic_ndms2.py @@ -5,18 +5,18 @@ https://home-assistant.io/components/device_tracker.keenetic_ndms2/ """ import logging -from collections import namedtuple -import requests import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.components.device_tracker import ( DOMAIN, PLATFORM_SCHEMA, DeviceScanner) from homeassistant.const import ( - CONF_HOST, CONF_PASSWORD, CONF_USERNAME + CONF_HOST, CONF_PORT, CONF_PASSWORD, CONF_USERNAME ) +REQUIREMENTS = ['ndms2_client==0.0.3'] + _LOGGER = logging.getLogger(__name__) # Interface name to track devices for. Most likely one will not need to @@ -25,11 +25,13 @@ CONF_INTERFACE = 'interface' DEFAULT_INTERFACE = 'Home' +DEFAULT_PORT = 23 PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_HOST): cv.string, vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PORT, default=DEFAULT_PORT): cv.port, vol.Required(CONF_PASSWORD): cv.string, vol.Required(CONF_INTERFACE, default=DEFAULT_INTERFACE): cv.string, }) @@ -42,21 +44,22 @@ def get_scanner(_hass, config): return scanner if scanner.success_init else None -Device = namedtuple('Device', ['mac', 'name']) - - class KeeneticNDMS2DeviceScanner(DeviceScanner): """This class scans for devices using keenetic NDMS2 web interface.""" def __init__(self, config): """Initialize the scanner.""" + from ndms2_client import Client, TelnetConnection self.last_results = [] - self._url = 'http://%s/rci/show/ip/arp' % config[CONF_HOST] self._interface = config[CONF_INTERFACE] - self._username = config.get(CONF_USERNAME) - self._password = config.get(CONF_PASSWORD) + self._client = Client(TelnetConnection( + config.get(CONF_HOST), + config.get(CONF_PORT), + config.get(CONF_USERNAME), + config.get(CONF_PASSWORD), + )) self.success_init = self._update_info() _LOGGER.info("Scanner initialized") @@ -69,53 +72,32 @@ def scan_devices(self): def get_device_name(self, device): """Return the name of the given device or None if we don't know.""" - filter_named = [result.name for result in self.last_results - if result.mac == device] - - if filter_named: - return filter_named[0] - return None + name = next(( + result.name for result in self.last_results + if result.mac == device), None) + return name + + def get_extra_attributes(self, device): + """Return the IP of the given device.""" + attributes = next(( + {'ip': result.ip} for result in self.last_results + if result.mac == device), {}) + return attributes def _update_info(self): """Get ARP from keenetic router.""" - _LOGGER.info("Fetching...") - - last_results = [] + _LOGGER.debug("Fetching devices from router...") - # doing a request - try: - from requests.auth import HTTPDigestAuth - res = requests.get(self._url, timeout=10, auth=HTTPDigestAuth( - self._username, self._password - )) - except requests.exceptions.Timeout: - _LOGGER.error( - "Connection to the router timed out at URL %s", self._url) - return False - if res.status_code != 200: - _LOGGER.error( - "Connection failed with http code %s", res.status_code) - return False + from ndms2_client import ConnectionException try: - result = res.json() - except ValueError: - # If json decoder could not parse the response - _LOGGER.error("Failed to parse response from router") + self.last_results = [ + dev + for dev in self._client.get_devices() + if dev.interface == self._interface + ] + _LOGGER.debug("Successfully fetched data from router") + return True + + except ConnectionException: + _LOGGER.error("Error fetching data from router") return False - - # parsing response - for info in result: - if info.get('interface') != self._interface: - continue - mac = info.get('mac') - name = info.get('name') - # No address = no item :) - if mac is None: - continue - - last_results.append(Device(mac.upper(), name)) - - self.last_results = last_results - - _LOGGER.info("Request successful") - return True diff --git a/requirements_all.txt b/requirements_all.txt index eda2b604115756..6dae4f9c44c37e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -579,6 +579,9 @@ nad_receiver==0.0.9 # homeassistant.components.light.nanoleaf_aurora nanoleaf==0.4.1 +# homeassistant.components.device_tracker.keenetic_ndms2 +ndms2_client==0.0.3 + # homeassistant.components.sensor.netdata netdata==0.1.2 From 8ee3b535ef3d24bc420075231b231c1f1c4e27a0 Mon Sep 17 00:00:00 2001 From: Andrey Date: Tue, 31 Jul 2018 17:00:17 +0300 Subject: [PATCH 006/117] Add disallow_untyped_calls to mypy check. (#15661) * Add disallow_untyped_calls to mypy check. * Fix generator --- homeassistant/auth/__init__.py | 7 +++++-- homeassistant/components/__init__.py | 3 ++- .../components/persistent_notification/__init__.py | 11 +++++++---- homeassistant/config_entries.py | 14 +++++++------- homeassistant/helpers/entity_values.py | 4 +++- homeassistant/helpers/signal.py | 6 +++--- mypy.ini | 2 ++ 7 files changed, 29 insertions(+), 18 deletions(-) diff --git a/homeassistant/auth/__init__.py b/homeassistant/auth/__init__.py index 62c416a988300d..35804cd8483cdc 100644 --- a/homeassistant/auth/__init__.py +++ b/homeassistant/auth/__init__.py @@ -2,9 +2,10 @@ import asyncio import logging from collections import OrderedDict +from typing import List, Awaitable from homeassistant import data_entry_flow -from homeassistant.core import callback +from homeassistant.core import callback, HomeAssistant from . import models from . import auth_store @@ -13,7 +14,9 @@ _LOGGER = logging.getLogger(__name__) -async def auth_manager_from_config(hass, provider_configs): +async def auth_manager_from_config( + hass: HomeAssistant, + provider_configs: List[dict]) -> Awaitable['AuthManager']: """Initialize an auth manager from config.""" store = auth_store.AuthStore(hass) if provider_configs: diff --git a/homeassistant/components/__init__.py b/homeassistant/components/__init__.py index 06e28c42b13c6f..bf1577cbf01f38 100644 --- a/homeassistant/components/__init__.py +++ b/homeassistant/components/__init__.py @@ -10,6 +10,7 @@ import asyncio import itertools as it import logging +from typing import Awaitable import homeassistant.core as ha import homeassistant.config as conf_util @@ -109,7 +110,7 @@ def async_reload_core_config(hass): @asyncio.coroutine -def async_setup(hass, config): +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): diff --git a/homeassistant/components/persistent_notification/__init__.py b/homeassistant/components/persistent_notification/__init__.py index cce3550d35c8f0..2850a5f96cd90b 100644 --- a/homeassistant/components/persistent_notification/__init__.py +++ b/homeassistant/components/persistent_notification/__init__.py @@ -6,10 +6,11 @@ """ import asyncio import logging +from typing import Awaitable import voluptuous as vol -from homeassistant.core import callback +from homeassistant.core import callback, HomeAssistant from homeassistant.exceptions import TemplateError from homeassistant.loader import bind_hass from homeassistant.helpers import config_validation as cv @@ -58,7 +59,8 @@ def dismiss(hass, notification_id): @callback @bind_hass -def async_create(hass, message, title=None, notification_id=None): +def async_create(hass: HomeAssistant, message: str, title: str = None, + notification_id: str = None) -> None: """Generate a notification.""" data = { key: value for key, value in [ @@ -68,7 +70,8 @@ def async_create(hass, message, title=None, notification_id=None): ] if value is not None } - hass.async_add_job(hass.services.async_call(DOMAIN, SERVICE_CREATE, data)) + hass.async_create_task( + hass.services.async_call(DOMAIN, SERVICE_CREATE, data)) @callback @@ -81,7 +84,7 @@ def async_dismiss(hass, notification_id): @asyncio.coroutine -def async_setup(hass, config): +def async_setup(hass: HomeAssistant, config: dict) -> Awaitable[bool]: """Set up the persistent notification component.""" @callback def create_service(call): diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 8e2bb3fa5df9c4..12420e989eea74 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -113,7 +113,7 @@ async def async_step_discovery(info): import logging import uuid -from typing import Set, Optional # noqa pylint: disable=unused-import +from typing import Set, Optional, List # noqa pylint: disable=unused-import from homeassistant import data_entry_flow from homeassistant.core import callback, HomeAssistant @@ -270,19 +270,19 @@ class ConfigEntries: An instance of this object is available via `hass.config_entries`. """ - def __init__(self, hass, hass_config): + def __init__(self, hass: HomeAssistant, hass_config: dict) -> None: """Initialize the entry manager.""" self.hass = hass self.flow = data_entry_flow.FlowManager( hass, self._async_create_flow, self._async_finish_flow) self._hass_config = hass_config - self._entries = None + self._entries = [] # type: List[ConfigEntry] self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) @callback - def async_domains(self): + def async_domains(self) -> List[str]: """Return domains for which we have entries.""" - seen = set() # type: Set[ConfigEntry] + seen = set() # type: Set[str] result = [] for entry in self._entries: @@ -293,7 +293,7 @@ def async_domains(self): return result @callback - def async_entries(self, domain=None): + def async_entries(self, domain: str = None) -> List[ConfigEntry]: """Return all entries or entries for a specific domain.""" if domain is None: return list(self._entries) @@ -319,7 +319,7 @@ async def async_remove(self, entry_id): 'require_restart': not unloaded } - async def async_load(self): + async def async_load(self) -> None: """Handle loading the config.""" # Migrating for config entries stored before 0.73 config = await self.hass.helpers.storage.async_migrator( diff --git a/homeassistant/helpers/entity_values.py b/homeassistant/helpers/entity_values.py index 5caa6b93131f24..77739f8adabbf8 100644 --- a/homeassistant/helpers/entity_values.py +++ b/homeassistant/helpers/entity_values.py @@ -2,6 +2,7 @@ from collections import OrderedDict import fnmatch import re +from typing import Dict from homeassistant.core import split_entity_id @@ -9,7 +10,8 @@ class EntityValues: """Class to store entity id based values.""" - def __init__(self, exact=None, domain=None, glob=None): + def __init__(self, exact: Dict = None, domain: Dict = None, + glob: Dict = None) -> None: """Initialize an EntityConfigDict.""" self._cache = {} self._exact = exact diff --git a/homeassistant/helpers/signal.py b/homeassistant/helpers/signal.py index 3ea52388d3388c..824b32177cdb54 100644 --- a/homeassistant/helpers/signal.py +++ b/homeassistant/helpers/signal.py @@ -3,7 +3,7 @@ import signal import sys -from homeassistant.core import callback +from homeassistant.core import callback, HomeAssistant from homeassistant.const import RESTART_EXIT_CODE from homeassistant.loader import bind_hass @@ -12,13 +12,13 @@ @callback @bind_hass -def async_register_signal_handling(hass): +def async_register_signal_handling(hass: HomeAssistant) -> None: """Register system signal handler for core.""" if sys.platform != 'win32': @callback def async_signal_handle(exit_code): """Wrap signal handling.""" - hass.async_add_job(hass.async_stop(exit_code)) + hass.async_create_task(hass.async_stop(exit_code)) try: hass.loop.add_signal_handler( diff --git a/mypy.ini b/mypy.ini index c92786e643fd5b..875aec5eda7993 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1,5 +1,6 @@ [mypy] check_untyped_defs = true +disallow_untyped_calls = true follow_imports = silent ignore_missing_imports = true warn_incomplete_stub = true @@ -16,4 +17,5 @@ disallow_untyped_defs = false [mypy-homeassistant.util.yaml] warn_return_any = false +disallow_untyped_calls = false From 5f214ffa98e648c368f9b30490423e930efcf3d8 Mon Sep 17 00:00:00 2001 From: John Arild Berentsen Date: Tue, 31 Jul 2018 16:14:14 +0200 Subject: [PATCH 007/117] Update pyozw to 0.4.9 (#15758) * update pyozw to 0.4.8 * add requirements_all.txt * use 0.4.9 --- homeassistant/components/zwave/__init__.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zwave/__init__.py b/homeassistant/components/zwave/__init__.py index e540259edd5544..8cf69e727027f6 100644 --- a/homeassistant/components/zwave/__init__.py +++ b/homeassistant/components/zwave/__init__.py @@ -35,7 +35,7 @@ from .util import (check_node_schema, check_value_schema, node_name, check_has_unique_id, is_node_parsed) -REQUIREMENTS = ['pydispatcher==2.0.5', 'python_openzwave==0.4.3'] +REQUIREMENTS = ['pydispatcher==2.0.5', 'python_openzwave==0.4.9'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 6dae4f9c44c37e..52fca6a7615704 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1123,7 +1123,7 @@ python-wink==1.9.1 python_opendata_transport==0.1.3 # homeassistant.components.zwave -python_openzwave==0.4.3 +python_openzwave==0.4.9 # homeassistant.components.egardia pythonegardia==1.0.39 From a4f9602405773fc543c803dc5ca37302a6d39d18 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Tue, 31 Jul 2018 19:11:29 +0200 Subject: [PATCH 008/117] Convert wind speed to km/h (fixes #15710) (#15740) * Convert wind speed to km/h (fixes #15710) * Round speed --- homeassistant/components/weather/openweathermap.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/weather/openweathermap.py b/homeassistant/components/weather/openweathermap.py index 334948b67fb51a..00d9bc47f1b92a 100644 --- a/homeassistant/components/weather/openweathermap.py +++ b/homeassistant/components/weather/openweathermap.py @@ -131,7 +131,7 @@ def humidity(self): @property def wind_speed(self): """Return the wind speed.""" - return self.data.get_wind().get('speed') + return round(self.data.get_wind().get('speed') * 3.6, 2) @property def wind_bearing(self): From 03847e6c416b20e04a1b7151a387110b427e6d6b Mon Sep 17 00:00:00 2001 From: priiduonu Date: Tue, 31 Jul 2018 20:18:11 +0300 Subject: [PATCH 009/117] Round precipitation forecast to 1 decimal place (#15759) The OWM returns precipitation forecast values as they are submitted to their network. It can lead to values like `0.0025000000000004 mm` which does not make sense and messes up the display card. This PR rounds the value to 1 decimal place. --- homeassistant/components/weather/openweathermap.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/weather/openweathermap.py b/homeassistant/components/weather/openweathermap.py index 00d9bc47f1b92a..46a0b3ecc14f3b 100644 --- a/homeassistant/components/weather/openweathermap.py +++ b/homeassistant/components/weather/openweathermap.py @@ -173,7 +173,10 @@ def forecast(self): ATTR_FORECAST_TEMP: entry.get_temperature('celsius').get('temp'), ATTR_FORECAST_PRECIPITATION: - entry.get_rain().get('3h'), + (round(entry.get_rain().get('3h'), 1) + if entry.get_rain().get('3h') is not None + and (round(entry.get_rain().get('3h'), 1) > 0) + else None), ATTR_FORECAST_CONDITION: [k for k, v in CONDITION_CLASSES.items() if entry.get_weather_code() in v][0] From d902a9f2799b041c1c1ead1465acc3dad92b2dce Mon Sep 17 00:00:00 2001 From: Scott Albertson Date: Tue, 31 Jul 2018 12:17:33 -0700 Subject: [PATCH 010/117] Add a "Reviewed by Hound" badge (#15767) --- README.rst | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 7f0d41b00eab98..6cf19d89c3c7c9 100644 --- a/README.rst +++ b/README.rst @@ -1,5 +1,5 @@ -Home Assistant |Build Status| |Coverage Status| |Chat Status| -============================================================= +Home Assistant |Build Status| |Coverage Status| |Chat Status| |Reviewed by Hound| +================================================================================= Home Assistant is a home automation platform running on Python 3. It is able to track and control all devices at home and offer a platform for automating control. @@ -33,6 +33,8 @@ of a component, check the `Home Assistant help section Date: Tue, 31 Jul 2018 21:17:55 +0200 Subject: [PATCH 011/117] Upgrade Mastodon.py to 1.3.1 (#15766) --- homeassistant/components/notify/mastodon.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/notify/mastodon.py b/homeassistant/components/notify/mastodon.py index e29289722e89b8..095e3f98ff91e3 100644 --- a/homeassistant/components/notify/mastodon.py +++ b/homeassistant/components/notify/mastodon.py @@ -13,7 +13,7 @@ from homeassistant.const import CONF_ACCESS_TOKEN import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['Mastodon.py==1.3.0'] +REQUIREMENTS = ['Mastodon.py==1.3.1'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 52fca6a7615704..5268a8b47c4792 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -33,7 +33,7 @@ DoorBirdPy==0.1.3 HAP-python==2.2.2 # homeassistant.components.notify.mastodon -Mastodon.py==1.3.0 +Mastodon.py==1.3.1 # homeassistant.components.isy994 PyISY==1.1.0 From 95da41aa153dbdcb157db285486e501c7a5c87e4 Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Tue, 31 Jul 2018 20:27:43 +0100 Subject: [PATCH 012/117] This component API has been decomissioned on the 31st of May 2018 by Telstra (#15757) See #15668 --- homeassistant/components/notify/telstra.py | 104 --------------------- 1 file changed, 104 deletions(-) delete mode 100644 homeassistant/components/notify/telstra.py diff --git a/homeassistant/components/notify/telstra.py b/homeassistant/components/notify/telstra.py deleted file mode 100644 index 82ac914a647cb2..00000000000000 --- a/homeassistant/components/notify/telstra.py +++ /dev/null @@ -1,104 +0,0 @@ -""" -Telstra API platform for notify component. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/notify.telstra/ -""" -import logging - -from aiohttp.hdrs import CONTENT_TYPE, AUTHORIZATION -import requests -import voluptuous as vol - -from homeassistant.components.notify import ( - ATTR_TITLE, PLATFORM_SCHEMA, BaseNotificationService) -from homeassistant.const import CONTENT_TYPE_JSON -import homeassistant.helpers.config_validation as cv - -_LOGGER = logging.getLogger(__name__) - -CONF_CONSUMER_KEY = 'consumer_key' -CONF_CONSUMER_SECRET = 'consumer_secret' -CONF_PHONE_NUMBER = 'phone_number' - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_CONSUMER_KEY): cv.string, - vol.Required(CONF_CONSUMER_SECRET): cv.string, - vol.Required(CONF_PHONE_NUMBER): cv.string, -}) - - -def get_service(hass, config, discovery_info=None): - """Get the Telstra SMS API notification service.""" - consumer_key = config.get(CONF_CONSUMER_KEY) - consumer_secret = config.get(CONF_CONSUMER_SECRET) - phone_number = config.get(CONF_PHONE_NUMBER) - - if _authenticate(consumer_key, consumer_secret) is False: - _LOGGER.exception("Error obtaining authorization from Telstra API") - return None - - return TelstraNotificationService( - consumer_key, consumer_secret, phone_number) - - -class TelstraNotificationService(BaseNotificationService): - """Implementation of a notification service for the Telstra SMS API.""" - - def __init__(self, consumer_key, consumer_secret, phone_number): - """Initialize the service.""" - self._consumer_key = consumer_key - self._consumer_secret = consumer_secret - self._phone_number = phone_number - - def send_message(self, message="", **kwargs): - """Send a message to a user.""" - title = kwargs.get(ATTR_TITLE) - - # Retrieve authorization first - token_response = _authenticate( - self._consumer_key, self._consumer_secret) - if token_response is False: - _LOGGER.exception("Error obtaining authorization from Telstra API") - return - - # Send the SMS - if title: - text = '{} {}'.format(title, message) - else: - text = message - - message_data = { - 'to': self._phone_number, - 'body': text, - } - message_resource = 'https://api.telstra.com/v1/sms/messages' - message_headers = { - CONTENT_TYPE: CONTENT_TYPE_JSON, - AUTHORIZATION: 'Bearer {}'.format(token_response['access_token']), - } - message_response = requests.post( - message_resource, headers=message_headers, json=message_data, - timeout=10) - - if message_response.status_code != 202: - _LOGGER.exception("Failed to send SMS. Status code: %d", - message_response.status_code) - - -def _authenticate(consumer_key, consumer_secret): - """Authenticate with the Telstra API.""" - token_data = { - 'client_id': consumer_key, - 'client_secret': consumer_secret, - 'grant_type': 'client_credentials', - 'scope': 'SMS' - } - token_resource = 'https://api.telstra.com/v1/oauth/token' - token_response = requests.get( - token_resource, params=token_data, timeout=10).json() - - if 'error' in token_response: - return False - - return token_response From a11c2a0bd85234197f130f6ff5117417d8a0a956 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Tue, 31 Jul 2018 21:39:37 +0200 Subject: [PATCH 013/117] Fix docstrings (#15770) --- homeassistant/components/onboarding/__init__.py | 5 +++-- homeassistant/components/onboarding/views.py | 10 +++++----- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/onboarding/__init__.py b/homeassistant/components/onboarding/__init__.py index 6dea5919f096d6..52d18b9a87063a 100644 --- a/homeassistant/components/onboarding/__init__.py +++ b/homeassistant/components/onboarding/__init__.py @@ -2,9 +2,10 @@ from homeassistant.core import callback from homeassistant.loader import bind_hass -from .const import STEPS, STEP_USER, DOMAIN +from .const import DOMAIN, STEP_USER, STEPS DEPENDENCIES = ['http'] + STORAGE_KEY = DOMAIN STORAGE_VERSION = 1 @@ -21,7 +22,7 @@ def async_is_onboarded(hass): async def async_setup(hass, config): - """Set up the onboard component.""" + """Set up the onboarding component.""" store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) data = await store.async_load() diff --git a/homeassistant/components/onboarding/views.py b/homeassistant/components/onboarding/views.py index 17d83003c48983..497fa827f083df 100644 --- a/homeassistant/components/onboarding/views.py +++ b/homeassistant/components/onboarding/views.py @@ -3,21 +3,21 @@ import voluptuous as vol -from homeassistant.core import callback -from homeassistant.components.http.view import HomeAssistantView from homeassistant.components.http.data_validator import RequestDataValidator +from homeassistant.components.http.view import HomeAssistantView +from homeassistant.core import callback -from .const import DOMAIN, STEPS, STEP_USER +from .const import DOMAIN, STEP_USER, STEPS async def async_setup(hass, data, store): - """Setup onboarding.""" + """Set up the onboarding view.""" hass.http.register_view(OnboardingView(data, store)) hass.http.register_view(UserOnboardingView(data, store)) class OnboardingView(HomeAssistantView): - """Returns the onboarding status.""" + """Return the onboarding status.""" requires_auth = False url = '/api/onboarding' From 3445dc1f006a6770e959f5dfb95bcc3f9db647f0 Mon Sep 17 00:00:00 2001 From: Mathieu Velten Date: Tue, 31 Jul 2018 21:40:13 +0200 Subject: [PATCH 014/117] Update pynetgear to 0.4.1 (bugfixes) (#15768) --- 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 0e48e3072b208a..87be70b2040a58 100644 --- a/homeassistant/components/device_tracker/netgear.py +++ b/homeassistant/components/device_tracker/netgear.py @@ -15,7 +15,7 @@ CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_PORT, CONF_SSL, CONF_DEVICES, CONF_EXCLUDE) -REQUIREMENTS = ['pynetgear==0.4.0'] +REQUIREMENTS = ['pynetgear==0.4.1'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 5268a8b47c4792..e498b833bd2b83 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -947,7 +947,7 @@ pymysensors==0.16.0 pynello==1.5.1 # homeassistant.components.device_tracker.netgear -pynetgear==0.4.0 +pynetgear==0.4.1 # homeassistant.components.switch.netio pynetio==0.1.6 From 0b6f2f5b9133d946f1ae50a085f612858c729c34 Mon Sep 17 00:00:00 2001 From: Ioan Loosley Date: Tue, 31 Jul 2018 20:45:18 +0100 Subject: [PATCH 015/117] Opensky altitude (#15273) * Added Altitude to opensky * decided to take all metadata * Final Tidy * More formatting * moving CONF_ALTITUDE to platform * Moved CONF_ALTITUDE to platform --- homeassistant/components/sensor/opensky.py | 59 +++++++++++++++------- 1 file changed, 40 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/sensor/opensky.py b/homeassistant/components/sensor/opensky.py index af0491cc26cf96..9178b46c488f6f 100644 --- a/homeassistant/components/sensor/opensky.py +++ b/homeassistant/components/sensor/opensky.py @@ -13,22 +13,27 @@ import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_NAME, CONF_LATITUDE, CONF_LONGITUDE, CONF_RADIUS, - ATTR_ATTRIBUTION, ATTR_LATITUDE, ATTR_LONGITUDE, - LENGTH_KILOMETERS, LENGTH_METERS) + CONF_NAME, CONF_LATITUDE, CONF_LONGITUDE, + CONF_RADIUS, ATTR_ATTRIBUTION, ATTR_LATITUDE, + ATTR_LONGITUDE, LENGTH_KILOMETERS, LENGTH_METERS) from homeassistant.helpers.entity import Entity from homeassistant.util import distance as util_distance from homeassistant.util import location as util_location _LOGGER = logging.getLogger(__name__) +CONF_ALTITUDE = 'altitude' + ATTR_CALLSIGN = 'callsign' +ATTR_ALTITUDE = 'altitude' ATTR_ON_GROUND = 'on_ground' ATTR_SENSOR = 'sensor' ATTR_STATES = 'states' DOMAIN = 'opensky' +DEFAULT_ALTITUDE = 0 + EVENT_OPENSKY_ENTRY = '{}_entry'.format(DOMAIN) EVENT_OPENSKY_EXIT = '{}_exit'.format(DOMAIN) SCAN_INTERVAL = timedelta(seconds=12) # opensky public limit is 10 seconds @@ -38,7 +43,7 @@ OPENSKY_API_URL = 'https://opensky-network.org/api/states/all' OPENSKY_API_FIELDS = [ 'icao24', ATTR_CALLSIGN, 'origin_country', 'time_position', - 'time_velocity', ATTR_LONGITUDE, ATTR_LATITUDE, 'altitude', + 'time_velocity', ATTR_LONGITUDE, ATTR_LATITUDE, ATTR_ALTITUDE, ATTR_ON_GROUND, 'velocity', 'heading', 'vertical_rate', 'sensors'] @@ -46,7 +51,8 @@ vol.Required(CONF_RADIUS): vol.Coerce(float), vol.Optional(CONF_NAME): cv.string, vol.Inclusive(CONF_LATITUDE, 'coordinates'): cv.latitude, - vol.Inclusive(CONF_LONGITUDE, 'coordinates'): cv.longitude + vol.Inclusive(CONF_LONGITUDE, 'coordinates'): cv.longitude, + vol.Optional(CONF_ALTITUDE, default=DEFAULT_ALTITUDE): vol.Coerce(float) }) @@ -56,19 +62,20 @@ def setup_platform(hass, config, add_devices, discovery_info=None): longitude = config.get(CONF_LONGITUDE, hass.config.longitude) add_devices([OpenSkySensor( hass, config.get(CONF_NAME, DOMAIN), latitude, longitude, - config.get(CONF_RADIUS))], True) + config.get(CONF_RADIUS), config.get(CONF_ALTITUDE))], True) class OpenSkySensor(Entity): """Open Sky Network Sensor.""" - def __init__(self, hass, name, latitude, longitude, radius): + def __init__(self, hass, name, latitude, longitude, radius, altitude): """Initialize the sensor.""" self._session = requests.Session() self._latitude = latitude self._longitude = longitude self._radius = util_distance.convert( radius, LENGTH_KILOMETERS, LENGTH_METERS) + self._altitude = altitude self._state = 0 self._hass = hass self._name = name @@ -84,11 +91,18 @@ def state(self): """Return the state of the sensor.""" return self._state - def _handle_boundary(self, callsigns, event): + def _handle_boundary(self, flights, event, metadata): """Handle flights crossing region boundary.""" - for callsign in callsigns: + for flight in flights: + if flight in metadata: + altitude = metadata[flight].get(ATTR_ALTITUDE) + else: + # Assume Flight has landed if missing. + altitude = 0 + data = { - ATTR_CALLSIGN: callsign, + ATTR_CALLSIGN: flight, + ATTR_ALTITUDE: altitude, ATTR_SENSOR: self._name, } self._hass.bus.fire(event, data) @@ -96,30 +110,37 @@ def _handle_boundary(self, callsigns, event): def update(self): """Update device state.""" currently_tracked = set() + flight_metadata = {} states = self._session.get(OPENSKY_API_URL).json().get(ATTR_STATES) for state in states: - data = dict(zip(OPENSKY_API_FIELDS, state)) + flight = dict(zip(OPENSKY_API_FIELDS, state)) + callsign = flight[ATTR_CALLSIGN].strip() + if callsign != '': + flight_metadata[callsign] = flight + else: + continue missing_location = ( - data.get(ATTR_LONGITUDE) is None or - data.get(ATTR_LATITUDE) is None) + flight.get(ATTR_LONGITUDE) is None or + flight.get(ATTR_LATITUDE) is None) if missing_location: continue - if data.get(ATTR_ON_GROUND): + if flight.get(ATTR_ON_GROUND): continue distance = util_location.distance( self._latitude, self._longitude, - data.get(ATTR_LATITUDE), data.get(ATTR_LONGITUDE)) + flight.get(ATTR_LATITUDE), flight.get(ATTR_LONGITUDE)) if distance is None or distance > self._radius: continue - callsign = data[ATTR_CALLSIGN].strip() - if callsign == '': + altitude = flight.get(ATTR_ALTITUDE) + if altitude > self._altitude and self._altitude != 0: continue currently_tracked.add(callsign) if self._previously_tracked is not None: entries = currently_tracked - self._previously_tracked exits = self._previously_tracked - currently_tracked - self._handle_boundary(entries, EVENT_OPENSKY_ENTRY) - self._handle_boundary(exits, EVENT_OPENSKY_EXIT) + self._handle_boundary(entries, EVENT_OPENSKY_ENTRY, + flight_metadata) + self._handle_boundary(exits, EVENT_OPENSKY_EXIT, flight_metadata) self._state = len(currently_tracked) self._previously_tracked = currently_tracked From 623f6c841bf0636c593221c43d22a33569d5e4fd Mon Sep 17 00:00:00 2001 From: Oscar Tin Yiu Lai Date: Wed, 1 Aug 2018 14:38:34 +1000 Subject: [PATCH 016/117] Expose internal states and fixed on/off state of Dyson Fans (#15716) * exposing internal state and fixed onoff state * fixed styling * revert file mode changes * removed self type hints * better unit test and changed the way to return attributes * made wolfie happy --- homeassistant/components/fan/dyson.py | 13 +++++++- tests/components/fan/test_dyson.py | 44 ++++++++++++++++++++++++++- 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/fan/dyson.py b/homeassistant/components/fan/dyson.py index fbe9ffc948cdd0..3eb4646e6dcbdc 100644 --- a/homeassistant/components/fan/dyson.py +++ b/homeassistant/components/fan/dyson.py @@ -18,6 +18,9 @@ CONF_NIGHT_MODE = 'night_mode' +ATTR_IS_NIGHT_MODE = 'is_night_mode' +ATTR_IS_AUTO_MODE = 'is_auto_mode' + DEPENDENCIES = ['dyson'] DYSON_FAN_DEVICES = 'dyson_fan_devices' @@ -158,7 +161,7 @@ def oscillating(self): def is_on(self): """Return true if the entity is on.""" if self._device.state: - return self._device.state.fan_state == "FAN" + return self._device.state.fan_mode == "FAN" return False @property @@ -232,3 +235,11 @@ def speed_list(self) -> list: def supported_features(self) -> int: """Flag supported features.""" return SUPPORT_OSCILLATE | SUPPORT_SET_SPEED + + @property + def device_state_attributes(self) -> dict: + """Return optional state attributes.""" + return { + ATTR_IS_NIGHT_MODE: self.is_night_mode, + ATTR_IS_AUTO_MODE: self.is_auto_mode + } diff --git a/tests/components/fan/test_dyson.py b/tests/components/fan/test_dyson.py index 49338e123e364c..2953ea2754ba02 100644 --- a/tests/components/fan/test_dyson.py +++ b/tests/components/fan/test_dyson.py @@ -2,8 +2,11 @@ import unittest from unittest import mock +from homeassistant.setup import setup_component +from homeassistant.components import dyson as dyson_parent from homeassistant.components.dyson import DYSON_DEVICES -from homeassistant.components.fan import dyson +from homeassistant.components.fan import (dyson, ATTR_SPEED, ATTR_SPEED_LIST, + ATTR_OSCILLATING) from tests.common import get_test_home_assistant from libpurecoollink.const import FanSpeed, FanMode, NightMode, Oscillation from libpurecoollink.dyson_pure_state import DysonPureCoolState @@ -91,6 +94,45 @@ def _add_device(devices): self.hass.data[dyson.DYSON_DEVICES] = [device_fan, device_non_fan] dyson.setup_platform(self.hass, None, _add_device) + @mock.patch('libpurecoollink.dyson.DysonAccount.devices', + return_value=[_get_device_on()]) + @mock.patch('libpurecoollink.dyson.DysonAccount.login', return_value=True) + def test_get_state_attributes(self, mocked_login, mocked_devices): + """Test async added to hass.""" + setup_component(self.hass, dyson_parent.DOMAIN, { + dyson_parent.DOMAIN: { + dyson_parent.CONF_USERNAME: "email", + dyson_parent.CONF_PASSWORD: "password", + dyson_parent.CONF_LANGUAGE: "US", + } + }) + self.hass.block_till_done() + state = self.hass.states.get("{}.{}".format( + dyson.DOMAIN, + mocked_devices.return_value[0].name)) + + assert dyson.ATTR_IS_NIGHT_MODE in state.attributes + assert dyson.ATTR_IS_AUTO_MODE in state.attributes + assert ATTR_SPEED in state.attributes + assert ATTR_SPEED_LIST in state.attributes + assert ATTR_OSCILLATING in state.attributes + + @mock.patch('libpurecoollink.dyson.DysonAccount.devices', + return_value=[_get_device_on()]) + @mock.patch('libpurecoollink.dyson.DysonAccount.login', return_value=True) + def test_async_added_to_hass(self, mocked_login, mocked_devices): + """Test async added to hass.""" + setup_component(self.hass, dyson_parent.DOMAIN, { + dyson_parent.DOMAIN: { + dyson_parent.CONF_USERNAME: "email", + dyson_parent.CONF_PASSWORD: "password", + dyson_parent.CONF_LANGUAGE: "US", + } + }) + self.hass.block_till_done() + self.assertEqual(len(self.hass.data[dyson.DYSON_DEVICES]), 1) + assert mocked_devices.return_value[0].add_message_listener.called + def test_dyson_set_speed(self): """Test set fan speed.""" device = _get_device_on() From f8a478946e7c560573c0ccc16c7b96cef20f9df8 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 1 Aug 2018 11:03:08 +0200 Subject: [PATCH 017/117] deCONZ - support for power plugs (#15752) * Initial commit for deCONZ switch support * Fix hound comment * Fix martins comment; platforms shouldn't depend on another platform * Fix existing tests * New tests * Clean up unnecessary methods * Bump requirement to v43 * Added device state attributes to light --- homeassistant/components/deconz/__init__.py | 4 +- homeassistant/components/deconz/const.py | 2 + homeassistant/components/light/deconz.py | 18 ++++- homeassistant/components/switch/deconz.py | 82 +++++++++++++++++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/deconz/test_init.py | 6 +- tests/components/light/test_deconz.py | 16 ++++ tests/components/switch/test_deconz.py | 90 +++++++++++++++++++++ 9 files changed, 212 insertions(+), 10 deletions(-) create mode 100644 homeassistant/components/switch/deconz.py create mode 100644 tests/components/switch/test_deconz.py diff --git a/homeassistant/components/deconz/__init__.py b/homeassistant/components/deconz/__init__.py index e0982c65f3344b..eacb31e3f8b171 100644 --- a/homeassistant/components/deconz/__init__.py +++ b/homeassistant/components/deconz/__init__.py @@ -22,7 +22,7 @@ CONF_ALLOW_CLIP_SENSOR, CONFIG_FILE, DATA_DECONZ_EVENT, DATA_DECONZ_ID, DATA_DECONZ_UNSUB, DOMAIN, _LOGGER) -REQUIREMENTS = ['pydeconz==42'] +REQUIREMENTS = ['pydeconz==43'] CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ @@ -96,7 +96,7 @@ def async_add_device_callback(device_type, device): hass.data[DATA_DECONZ_EVENT] = [] hass.data[DATA_DECONZ_UNSUB] = [] - for component in ['binary_sensor', 'light', 'scene', 'sensor']: + for component in ['binary_sensor', 'light', 'scene', 'sensor', 'switch']: hass.async_create_task(hass.config_entries.async_forward_entry_setup( config_entry, component)) diff --git a/homeassistant/components/deconz/const.py b/homeassistant/components/deconz/const.py index 6deee322a15e31..7e16a9d7f107e0 100644 --- a/homeassistant/components/deconz/const.py +++ b/homeassistant/components/deconz/const.py @@ -14,3 +14,5 @@ ATTR_DARK = 'dark' ATTR_ON = 'on' + +SWITCH_TYPES = ["On/Off plug-in unit", "Smart plug"] diff --git a/homeassistant/components/light/deconz.py b/homeassistant/components/light/deconz.py index 08d7f5773f7579..4e1f5d8f15f9d5 100644 --- a/homeassistant/components/light/deconz.py +++ b/homeassistant/components/light/deconz.py @@ -4,9 +4,9 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/light.deconz/ """ -from homeassistant.components.deconz import ( - DOMAIN as DATA_DECONZ, DATA_DECONZ_ID, DATA_DECONZ_UNSUB) -from homeassistant.components.deconz.const import CONF_ALLOW_DECONZ_GROUPS +from homeassistant.components.deconz.const import ( + CONF_ALLOW_DECONZ_GROUPS, DOMAIN as DATA_DECONZ, + DATA_DECONZ_ID, DATA_DECONZ_UNSUB, SWITCH_TYPES) from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_FLASH, ATTR_HS_COLOR, ATTR_TRANSITION, EFFECT_COLORLOOP, FLASH_LONG, FLASH_SHORT, @@ -32,7 +32,8 @@ def async_add_light(lights): """Add light from deCONZ.""" entities = [] for light in lights: - entities.append(DeconzLight(light)) + if light.type not in SWITCH_TYPES: + entities.append(DeconzLight(light)) async_add_devices(entities, True) hass.data[DATA_DECONZ_UNSUB].append( @@ -186,3 +187,12 @@ async def async_turn_off(self, **kwargs): del data['on'] await self._light.async_set_state(data) + + @property + def device_state_attributes(self): + """Return the device state attributes.""" + attributes = {} + attributes['is_deconz_group'] = self._light.type == 'LightGroup' + if self._light.type == 'LightGroup': + attributes['all_on'] = self._light.all_on + return attributes diff --git a/homeassistant/components/switch/deconz.py b/homeassistant/components/switch/deconz.py new file mode 100644 index 00000000000000..95e7d7367392db --- /dev/null +++ b/homeassistant/components/switch/deconz.py @@ -0,0 +1,82 @@ +""" +Support for deCONZ switches. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/switch.deconz/ +""" +from homeassistant.components.deconz.const import ( + DOMAIN as DATA_DECONZ, DATA_DECONZ_ID, DATA_DECONZ_UNSUB, SWITCH_TYPES) +from homeassistant.components.switch import SwitchDevice +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +DEPENDENCIES = ['deconz'] + + +async def async_setup_platform(hass, config, async_add_devices, + discovery_info=None): + """Old way of setting up deCONZ switches.""" + pass + + +async def async_setup_entry(hass, config_entry, async_add_devices): + """Set up switches for deCONZ component. + + Switches are based same device class as lights in deCONZ. + """ + @callback + def async_add_switch(lights): + """Add switch from deCONZ.""" + entities = [] + for light in lights: + if light.type in SWITCH_TYPES: + entities.append(DeconzSwitch(light)) + async_add_devices(entities, True) + + hass.data[DATA_DECONZ_UNSUB].append( + async_dispatcher_connect(hass, 'deconz_new_light', async_add_switch)) + + async_add_switch(hass.data[DATA_DECONZ].lights.values()) + + +class DeconzSwitch(SwitchDevice): + """Representation of a deCONZ switch.""" + + def __init__(self, switch): + """Set up switch and add update callback to get data from websocket.""" + self._switch = switch + + async def async_added_to_hass(self): + """Subscribe to switches events.""" + self._switch.register_async_callback(self.async_update_callback) + self.hass.data[DATA_DECONZ_ID][self.entity_id] = self._switch.deconz_id + + @callback + def async_update_callback(self, reason): + """Update the switch's state.""" + self.async_schedule_update_ha_state() + + @property + def is_on(self): + """Return true if switch is on.""" + return self._switch.state + + @property + def name(self): + """Return the name of the switch.""" + return self._switch.name + + @property + def unique_id(self): + """Return a unique identifier for this switch.""" + return self._switch.uniqueid + + async def async_turn_on(self, **kwargs): + """Turn on switch.""" + data = {'on': True} + await self._switch.async_set_state(data) + + async def async_turn_off(self, **kwargs): + """Turn off switch.""" + data = {'on': False} + await self._switch.async_set_state(data) diff --git a/requirements_all.txt b/requirements_all.txt index e498b833bd2b83..6388842e84d94c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -793,7 +793,7 @@ pycsspeechtts==1.0.2 pydaikin==0.4 # homeassistant.components.deconz -pydeconz==42 +pydeconz==43 # homeassistant.components.zwave pydispatcher==2.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ff95cd3be25fef..3d8402842f1a12 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -136,7 +136,7 @@ py-canary==0.5.0 pyblackbird==0.5 # homeassistant.components.deconz -pydeconz==42 +pydeconz==43 # homeassistant.components.zwave pydispatcher==2.0.5 diff --git a/tests/components/deconz/test_init.py b/tests/components/deconz/test_init.py index 8f5342de1e3ad5..c6fc130a4a41a6 100644 --- a/tests/components/deconz/test_init.py +++ b/tests/components/deconz/test_init.py @@ -99,8 +99,8 @@ async def test_setup_entry_successful(hass): assert hass.data[deconz.DOMAIN] assert hass.data[deconz.DATA_DECONZ_ID] == {} assert len(hass.data[deconz.DATA_DECONZ_UNSUB]) == 1 - assert len(mock_add_job.mock_calls) == 4 - assert len(mock_config_entries.async_forward_entry_setup.mock_calls) == 4 + assert len(mock_add_job.mock_calls) == 5 + assert len(mock_config_entries.async_forward_entry_setup.mock_calls) == 5 assert mock_config_entries.async_forward_entry_setup.mock_calls[0][1] == \ (entry, 'binary_sensor') assert mock_config_entries.async_forward_entry_setup.mock_calls[1][1] == \ @@ -109,6 +109,8 @@ async def test_setup_entry_successful(hass): (entry, 'scene') assert mock_config_entries.async_forward_entry_setup.mock_calls[3][1] == \ (entry, 'sensor') + assert mock_config_entries.async_forward_entry_setup.mock_calls[4][1] == \ + (entry, 'switch') async def test_unload_entry(hass): diff --git a/tests/components/light/test_deconz.py b/tests/components/light/test_deconz.py index d7d609f820eb5f..df088d7a1b5dde 100644 --- a/tests/components/light/test_deconz.py +++ b/tests/components/light/test_deconz.py @@ -37,6 +37,15 @@ }, } +SWITCH = { + "1": { + "id": "Switch 1 id", + "name": "Switch 1 name", + "type": "On/Off plug-in unit", + "state": {} + } +} + async def setup_bridge(hass, data, allow_deconz_groups=True): """Load the deCONZ light platform.""" @@ -112,3 +121,10 @@ async def test_do_not_add_deconz_groups(hass): async_dispatcher_send(hass, 'deconz_new_group', [group]) await hass.async_block_till_done() assert len(hass.data[deconz.DATA_DECONZ_ID]) == 0 + + +async def test_no_switch(hass): + """Test that a switch doesn't get created as a light entity.""" + await setup_bridge(hass, {"lights": SWITCH}) + assert len(hass.data[deconz.DATA_DECONZ_ID]) == 0 + assert len(hass.states.async_all()) == 0 diff --git a/tests/components/switch/test_deconz.py b/tests/components/switch/test_deconz.py new file mode 100644 index 00000000000000..490a0e67c9dd59 --- /dev/null +++ b/tests/components/switch/test_deconz.py @@ -0,0 +1,90 @@ +"""deCONZ switch platform tests.""" +from unittest.mock import Mock, patch + +from homeassistant import config_entries +from homeassistant.components import deconz +from homeassistant.components.deconz.const import SWITCH_TYPES +from homeassistant.helpers.dispatcher import async_dispatcher_send + +from tests.common import mock_coro + +SUPPORTED_SWITCHES = { + "1": { + "id": "Switch 1 id", + "name": "Switch 1 name", + "type": "On/Off plug-in unit", + "state": {} + }, + "2": { + "id": "Switch 2 id", + "name": "Switch 2 name", + "type": "Smart plug", + "state": {} + } +} + +UNSUPPORTED_SWITCH = { + "1": { + "id": "Switch id", + "name": "Unsupported switch", + "type": "Not a smart plug", + "state": {} + } +} + + +async def setup_bridge(hass, data): + """Load the deCONZ switch platform.""" + from pydeconz import DeconzSession + loop = Mock() + session = Mock() + entry = Mock() + entry.data = {'host': '1.2.3.4', 'port': 80, 'api_key': '1234567890ABCDEF'} + bridge = DeconzSession(loop, session, **entry.data) + with patch('pydeconz.DeconzSession.async_get_state', + return_value=mock_coro(data)): + await bridge.async_load_parameters() + hass.data[deconz.DOMAIN] = bridge + hass.data[deconz.DATA_DECONZ_UNSUB] = [] + hass.data[deconz.DATA_DECONZ_ID] = {} + config_entry = config_entries.ConfigEntry( + 1, deconz.DOMAIN, 'Mock Title', {'host': 'mock-host'}, 'test') + await hass.config_entries.async_forward_entry_setup(config_entry, 'switch') + # To flush out the service call to update the group + await hass.async_block_till_done() + + +async def test_no_switches(hass): + """Test that no switch entities are created.""" + data = {} + await setup_bridge(hass, data) + assert len(hass.data[deconz.DATA_DECONZ_ID]) == 0 + assert len(hass.states.async_all()) == 0 + + +async def test_switch(hass): + """Test that all supported switch entities and switch group are created.""" + await setup_bridge(hass, {"lights": SUPPORTED_SWITCHES}) + assert "switch.switch_1_name" in hass.data[deconz.DATA_DECONZ_ID] + assert "switch.switch_2_name" in hass.data[deconz.DATA_DECONZ_ID] + assert len(SUPPORTED_SWITCHES) == len(SWITCH_TYPES) + assert len(hass.states.async_all()) == 3 + + +async def test_add_new_switch(hass): + """Test successful creation of switch entity.""" + data = {} + await setup_bridge(hass, data) + switch = Mock() + switch.name = 'name' + switch.type = "Smart plug" + switch.register_async_callback = Mock() + async_dispatcher_send(hass, 'deconz_new_light', [switch]) + await hass.async_block_till_done() + assert "switch.name" in hass.data[deconz.DATA_DECONZ_ID] + + +async def test_unsupported_switch(hass): + """Test that unsupported switches are not created.""" + await setup_bridge(hass, {"lights": UNSUPPORTED_SWITCH}) + assert len(hass.states.async_all()) == 0 From 2ff5b4ce9547e1d0b15904ee1eb74906eb8f7608 Mon Sep 17 00:00:00 2001 From: Conrad Juhl Andersen Date: Wed, 1 Aug 2018 14:51:38 +0200 Subject: [PATCH 018/117] Add support for STATES of vacuums (#15573) * Vacuum: Added support for STATES * Added debug logging and corrected state order * typo * Fix travis error, STATE = STATE for readability * status -> state * Changed to Entity instead of ToogleEntity * Updated some vacuums * Revert changes * Revert Changes * added SUPPORT_STATE * Woof? * Implement on/off if STATE not supported * Moved new state vaccum to Class StateVacuumDevice * Error: I should go to bed * Moved around methods for easier reading * Added StateVacuumDevice demo vacuum * Added tests for StateVacuumDevice demo vacuum * Fix styling errors * Refactored to BaseVaccum * Vacuum will now go back to dock * Class BaseVacuum is for internal use only * return -> await * return -> await --- homeassistant/components/vacuum/__init__.py | 221 ++++++++++++-------- homeassistant/components/vacuum/demo.py | 125 ++++++++++- tests/components/vacuum/test_demo.py | 77 ++++++- 3 files changed, 334 insertions(+), 89 deletions(-) diff --git a/homeassistant/components/vacuum/__init__.py b/homeassistant/components/vacuum/__init__.py index 880b3604a86a49..9cd9fd1c729673 100644 --- a/homeassistant/components/vacuum/__init__.py +++ b/homeassistant/components/vacuum/__init__.py @@ -14,12 +14,12 @@ from homeassistant.components import group from homeassistant.const import ( ATTR_BATTERY_LEVEL, ATTR_COMMAND, ATTR_ENTITY_ID, SERVICE_TOGGLE, - SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_ON) + SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_ON, STATE_PAUSED, STATE_IDLE) from homeassistant.loader import bind_hass import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa from homeassistant.helpers.entity_component import EntityComponent -from homeassistant.helpers.entity import ToggleEntity +from homeassistant.helpers.entity import (ToggleEntity, Entity) from homeassistant.helpers.icon import icon_for_battery_level _LOGGER = logging.getLogger(__name__) @@ -75,6 +75,13 @@ 'schema': VACUUM_SEND_COMMAND_SERVICE_SCHEMA}, } +STATE_CLEANING = 'cleaning' +STATE_DOCKED = 'docked' +STATE_IDLE = STATE_IDLE +STATE_PAUSED = STATE_PAUSED +STATE_RETURNING = 'returning' +STATE_ERROR = 'error' + DEFAULT_NAME = 'Vacuum cleaner robot' SUPPORT_TURN_ON = 1 @@ -89,6 +96,7 @@ SUPPORT_LOCATE = 512 SUPPORT_CLEAN_SPOT = 1024 SUPPORT_MAP = 2048 +SUPPORT_STATE = 4096 @bind_hass @@ -208,33 +216,22 @@ def async_handle_vacuum_service(service): return True -class VacuumDevice(ToggleEntity): - """Representation of a vacuum cleaner robot.""" +class _BaseVacuum(Entity): + """Representation of a base vacuum. + + Contains common properties and functions for all vacuum devices. + """ @property def supported_features(self): """Flag vacuum cleaner features that are supported.""" raise NotImplementedError() - @property - def status(self): - """Return the status of the vacuum cleaner.""" - return None - @property def battery_level(self): """Return the battery level of the vacuum cleaner.""" return None - @property - def battery_icon(self): - """Return the battery icon for the vacuum cleaner.""" - charging = False - if self.status is not None: - charging = 'charg' in self.status.lower() - return icon_for_battery_level( - battery_level=self.battery_level, charging=charging) - @property def fan_speed(self): """Return the fan speed of the vacuum cleaner.""" @@ -245,122 +242,176 @@ def fan_speed_list(self): """Get the list of available fan speed steps of the vacuum cleaner.""" raise NotImplementedError() - @property - def state_attributes(self): - """Return the state attributes of the vacuum cleaner.""" - data = {} - - if self.status is not None: - data[ATTR_STATUS] = self.status - - if self.battery_level is not None: - data[ATTR_BATTERY_LEVEL] = self.battery_level - data[ATTR_BATTERY_ICON] = self.battery_icon - - if self.fan_speed is not None: - data[ATTR_FAN_SPEED] = self.fan_speed - data[ATTR_FAN_SPEED_LIST] = self.fan_speed_list - - return data - - def turn_on(self, **kwargs): - """Turn the vacuum on and start cleaning.""" + def start_pause(self, **kwargs): + """Start, pause or resume the cleaning task.""" raise NotImplementedError() - def async_turn_on(self, **kwargs): - """Turn the vacuum on and start cleaning. + async def async_start_pause(self, **kwargs): + """Start, pause or resume the cleaning task. - This method must be run in the event loop and returns a coroutine. + This method must be run in the event loop. """ - return self.hass.async_add_job(partial(self.turn_on, **kwargs)) + await self.hass.async_add_executor_job( + partial(self.start_pause, **kwargs)) - def turn_off(self, **kwargs): - """Turn the vacuum off stopping the cleaning and returning home.""" + def stop(self, **kwargs): + """Stop the vacuum cleaner.""" raise NotImplementedError() - def async_turn_off(self, **kwargs): - """Turn the vacuum off stopping the cleaning and returning home. + async def async_stop(self, **kwargs): + """Stop the vacuum cleaner. - This method must be run in the event loop and returns a coroutine. + This method must be run in the event loop. """ - return self.hass.async_add_job(partial(self.turn_off, **kwargs)) + await self.hass.async_add_executor_job(partial(self.stop, **kwargs)) def return_to_base(self, **kwargs): """Set the vacuum cleaner to return to the dock.""" raise NotImplementedError() - def async_return_to_base(self, **kwargs): + async def async_return_to_base(self, **kwargs): """Set the vacuum cleaner to return to the dock. - This method must be run in the event loop and returns a coroutine. + This method must be run in the event loop. """ - return self.hass.async_add_job(partial(self.return_to_base, **kwargs)) - - def stop(self, **kwargs): - """Stop the vacuum cleaner.""" - raise NotImplementedError() - - def async_stop(self, **kwargs): - """Stop the vacuum cleaner. - - This method must be run in the event loop and returns a coroutine. - """ - return self.hass.async_add_job(partial(self.stop, **kwargs)) + await self.hass.async_add_executor_job( + partial(self.return_to_base, **kwargs)) def clean_spot(self, **kwargs): """Perform a spot clean-up.""" raise NotImplementedError() - def async_clean_spot(self, **kwargs): + async def async_clean_spot(self, **kwargs): """Perform a spot clean-up. - This method must be run in the event loop and returns a coroutine. + This method must be run in the event loop. """ - return self.hass.async_add_job(partial(self.clean_spot, **kwargs)) + await self.hass.async_add_executor_job( + partial(self.clean_spot, **kwargs)) def locate(self, **kwargs): """Locate the vacuum cleaner.""" raise NotImplementedError() - def async_locate(self, **kwargs): + async def async_locate(self, **kwargs): """Locate the vacuum cleaner. - This method must be run in the event loop and returns a coroutine. + This method must be run in the event loop. """ - return self.hass.async_add_job(partial(self.locate, **kwargs)) + await self.hass.async_add_executor_job(partial(self.locate, **kwargs)) def set_fan_speed(self, fan_speed, **kwargs): """Set fan speed.""" raise NotImplementedError() - def async_set_fan_speed(self, fan_speed, **kwargs): + async def async_set_fan_speed(self, fan_speed, **kwargs): """Set fan speed. - This method must be run in the event loop and returns a coroutine. + This method must be run in the event loop. """ - return self.hass.async_add_job( + await self.hass.async_add_executor_job( partial(self.set_fan_speed, fan_speed, **kwargs)) - def start_pause(self, **kwargs): - """Start, pause or resume the cleaning task.""" + def send_command(self, command, params=None, **kwargs): + """Send a command to a vacuum cleaner.""" raise NotImplementedError() - def async_start_pause(self, **kwargs): - """Start, pause or resume the cleaning task. + async def async_send_command(self, command, params=None, **kwargs): + """Send a command to a vacuum cleaner. - This method must be run in the event loop and returns a coroutine. + This method must be run in the event loop. """ - return self.hass.async_add_job( - partial(self.start_pause, **kwargs)) + await self.hass.async_add_executor_job( + partial(self.send_command, command, params=params, **kwargs)) - def send_command(self, command, params=None, **kwargs): - """Send a command to a vacuum cleaner.""" + +class VacuumDevice(_BaseVacuum, ToggleEntity): + """Representation of a vacuum cleaner robot.""" + + @property + def status(self): + """Return the status of the vacuum cleaner.""" + return None + + @property + def battery_icon(self): + """Return the battery icon for the vacuum cleaner.""" + charging = False + if self.status is not None: + charging = 'charg' in self.status.lower() + return icon_for_battery_level( + battery_level=self.battery_level, charging=charging) + + @property + def state_attributes(self): + """Return the state attributes of the vacuum cleaner.""" + data = {} + + if self.status is not None: + data[ATTR_STATUS] = self.status + + if self.battery_level is not None: + data[ATTR_BATTERY_LEVEL] = self.battery_level + data[ATTR_BATTERY_ICON] = self.battery_icon + + if self.fan_speed is not None: + data[ATTR_FAN_SPEED] = self.fan_speed + data[ATTR_FAN_SPEED_LIST] = self.fan_speed_list + + return data + + def turn_on(self, **kwargs): + """Turn the vacuum on and start cleaning.""" raise NotImplementedError() - def async_send_command(self, command, params=None, **kwargs): - """Send a command to a vacuum cleaner. + async def async_turn_on(self, **kwargs): + """Turn the vacuum on and start cleaning. - This method must be run in the event loop and returns a coroutine. + This method must be run in the event loop. """ - return self.hass.async_add_job( - partial(self.send_command, command, params=params, **kwargs)) + await self.hass.async_add_executor_job( + partial(self.turn_on, **kwargs)) + + def turn_off(self, **kwargs): + """Turn the vacuum off stopping the cleaning and returning home.""" + raise NotImplementedError() + + async def async_turn_off(self, **kwargs): + """Turn the vacuum off stopping the cleaning and returning home. + + This method must be run in the event loop. + """ + await self.hass.async_add_executor_job( + partial(self.turn_off, **kwargs)) + + +class StateVacuumDevice(_BaseVacuum): + """Representation of a vacuum cleaner robot that supports states.""" + + @property + def state(self): + """Return the state of the vacuum cleaner.""" + return None + + @property + def battery_icon(self): + """Return the battery icon for the vacuum cleaner.""" + charging = bool(self.state == STATE_DOCKED) + + return icon_for_battery_level( + battery_level=self.battery_level, charging=charging) + + @property + def state_attributes(self): + """Return the state attributes of the vacuum cleaner.""" + data = {} + + if self.battery_level is not None: + data[ATTR_BATTERY_LEVEL] = self.battery_level + data[ATTR_BATTERY_ICON] = self.battery_icon + + if self.fan_speed is not None: + data[ATTR_FAN_SPEED] = self.fan_speed + data[ATTR_FAN_SPEED_LIST] = self.fan_speed_list + + return data diff --git a/homeassistant/components/vacuum/demo.py b/homeassistant/components/vacuum/demo.py index 45fd8de269612e..737be5e857b6af 100644 --- a/homeassistant/components/vacuum/demo.py +++ b/homeassistant/components/vacuum/demo.py @@ -10,7 +10,9 @@ ATTR_CLEANED_AREA, SUPPORT_BATTERY, SUPPORT_CLEAN_SPOT, SUPPORT_FAN_SPEED, SUPPORT_LOCATE, SUPPORT_PAUSE, SUPPORT_RETURN_HOME, SUPPORT_SEND_COMMAND, SUPPORT_STATUS, SUPPORT_STOP, SUPPORT_TURN_OFF, - SUPPORT_TURN_ON, VacuumDevice) + SUPPORT_TURN_ON, SUPPORT_STATE, STATE_CLEANING, STATE_DOCKED, + STATE_IDLE, STATE_PAUSED, STATE_RETURNING, VacuumDevice, + StateVacuumDevice) _LOGGER = logging.getLogger(__name__) @@ -28,12 +30,17 @@ SUPPORT_LOCATE | SUPPORT_STATUS | SUPPORT_BATTERY | \ SUPPORT_CLEAN_SPOT +SUPPORT_STATE_SERVICES = SUPPORT_STATE | SUPPORT_PAUSE | SUPPORT_STOP | \ + SUPPORT_RETURN_HOME | SUPPORT_FAN_SPEED | \ + SUPPORT_BATTERY | SUPPORT_CLEAN_SPOT + FAN_SPEEDS = ['min', 'medium', 'high', 'max'] DEMO_VACUUM_COMPLETE = '0_Ground_floor' DEMO_VACUUM_MOST = '1_First_floor' DEMO_VACUUM_BASIC = '2_Second_floor' DEMO_VACUUM_MINIMAL = '3_Third_floor' DEMO_VACUUM_NONE = '4_Fourth_floor' +DEMO_VACUUM_STATE = '5_Fifth_floor' def setup_platform(hass, config, add_devices, discovery_info=None): @@ -44,6 +51,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): DemoVacuum(DEMO_VACUUM_BASIC, SUPPORT_BASIC_SERVICES), DemoVacuum(DEMO_VACUUM_MINIMAL, SUPPORT_MINIMAL_SERVICES), DemoVacuum(DEMO_VACUUM_NONE, 0), + StateDemoVacuum(DEMO_VACUUM_STATE), ]) @@ -204,3 +212,118 @@ def send_command(self, command, params=None, **kwargs): self._status = 'Executing {}({})'.format(command, params) self._state = True self.schedule_update_ha_state() + + +class StateDemoVacuum(StateVacuumDevice): + """Representation of a demo vacuum supporting states.""" + + def __init__(self, name): + """Initialize the vacuum.""" + self._name = name + self._supported_features = SUPPORT_STATE_SERVICES + self._state = STATE_DOCKED + self._fan_speed = FAN_SPEEDS[1] + self._cleaned_area = 0 + self._battery_level = 100 + + @property + def name(self): + """Return the name of the vacuum.""" + return self._name + + @property + def should_poll(self): + """No polling needed for a demo vacuum.""" + return False + + @property + def supported_features(self): + """Flag supported features.""" + return self._supported_features + + @property + def state(self): + """Return the current state of the vacuum.""" + return self._state + + @property + def battery_level(self): + """Return the current battery level of the vacuum.""" + if self.supported_features & SUPPORT_BATTERY == 0: + return + + return max(0, min(100, self._battery_level)) + + @property + def fan_speed(self): + """Return the current fan speed of the vacuum.""" + if self.supported_features & SUPPORT_FAN_SPEED == 0: + return + + return self._fan_speed + + @property + def fan_speed_list(self): + """Return the list of supported fan speeds.""" + if self.supported_features & SUPPORT_FAN_SPEED == 0: + return + return FAN_SPEEDS + + @property + def device_state_attributes(self): + """Return device state attributes.""" + return {ATTR_CLEANED_AREA: round(self._cleaned_area, 2)} + + def start_pause(self, **kwargs): + """Start, pause or resume the cleaning task.""" + if self.supported_features & SUPPORT_PAUSE == 0: + return + + if self._state == STATE_CLEANING: + self._state = STATE_PAUSED + else: + self._state = STATE_CLEANING + self._cleaned_area += 1.32 + self._battery_level -= 1 + self.schedule_update_ha_state() + + def stop(self, **kwargs): + """Stop the cleaning task, do not return to dock.""" + if self.supported_features & SUPPORT_STOP == 0: + return + + self._state = STATE_IDLE + self.schedule_update_ha_state() + + def return_to_base(self, **kwargs): + """Return dock to charging base.""" + if self.supported_features & SUPPORT_RETURN_HOME == 0: + return + + self._state = STATE_RETURNING + self.schedule_update_ha_state() + + self.hass.loop.call_later(30, self.__set_state_to_dock) + + def clean_spot(self, **kwargs): + """Perform a spot clean-up.""" + if self.supported_features & SUPPORT_CLEAN_SPOT == 0: + return + + self._state = STATE_CLEANING + self._cleaned_area += 1.32 + self._battery_level -= 1 + self.schedule_update_ha_state() + + def set_fan_speed(self, fan_speed, **kwargs): + """Set the vacuum's fan speed.""" + if self.supported_features & SUPPORT_FAN_SPEED == 0: + return + + if fan_speed in self.fan_speed_list: + self._fan_speed = fan_speed + self.schedule_update_ha_state() + + def __set_state_to_dock(self): + self._state = STATE_DOCKED + self.schedule_update_ha_state() diff --git a/tests/components/vacuum/test_demo.py b/tests/components/vacuum/test_demo.py index fadafdbc15e323..b6c96567f5075c 100644 --- a/tests/components/vacuum/test_demo.py +++ b/tests/components/vacuum/test_demo.py @@ -6,10 +6,12 @@ ATTR_BATTERY_LEVEL, ATTR_COMMAND, ATTR_ENTITY_ID, ATTR_FAN_SPEED, ATTR_FAN_SPEED_LIST, ATTR_PARAMS, ATTR_STATUS, DOMAIN, ENTITY_ID_ALL_VACUUMS, - SERVICE_SEND_COMMAND, SERVICE_SET_FAN_SPEED) + SERVICE_SEND_COMMAND, SERVICE_SET_FAN_SPEED, + STATE_DOCKED, STATE_CLEANING, STATE_PAUSED, STATE_IDLE, + STATE_RETURNING) from homeassistant.components.vacuum.demo import ( DEMO_VACUUM_BASIC, DEMO_VACUUM_COMPLETE, DEMO_VACUUM_MINIMAL, - DEMO_VACUUM_MOST, DEMO_VACUUM_NONE, FAN_SPEEDS) + DEMO_VACUUM_MOST, DEMO_VACUUM_NONE, DEMO_VACUUM_STATE, FAN_SPEEDS) from homeassistant.const import ( ATTR_SUPPORTED_FEATURES, CONF_PLATFORM, STATE_OFF, STATE_ON) from homeassistant.setup import setup_component @@ -21,6 +23,7 @@ ENTITY_VACUUM_MINIMAL = '{}.{}'.format(DOMAIN, DEMO_VACUUM_MINIMAL).lower() ENTITY_VACUUM_MOST = '{}.{}'.format(DOMAIN, DEMO_VACUUM_MOST).lower() ENTITY_VACUUM_NONE = '{}.{}'.format(DOMAIN, DEMO_VACUUM_NONE).lower() +ENTITY_VACUUM_STATE = '{}.{}'.format(DOMAIN, DEMO_VACUUM_STATE).lower() class TestVacuumDemo(unittest.TestCase): @@ -79,6 +82,14 @@ def test_supported_features(self): self.assertEqual(None, state.attributes.get(ATTR_FAN_SPEED_LIST)) self.assertEqual(STATE_OFF, state.state) + state = self.hass.states.get(ENTITY_VACUUM_STATE) + self.assertEqual(5244, state.attributes.get(ATTR_SUPPORTED_FEATURES)) + self.assertEqual(STATE_DOCKED, state.state) + self.assertEqual(100, state.attributes.get(ATTR_BATTERY_LEVEL)) + self.assertEqual("medium", state.attributes.get(ATTR_FAN_SPEED)) + self.assertListEqual(FAN_SPEEDS, + state.attributes.get(ATTR_FAN_SPEED_LIST)) + def test_methods(self): """Test if methods call the services as expected.""" self.hass.states.set(ENTITY_VACUUM_BASIC, STATE_ON) @@ -147,6 +158,41 @@ def test_methods(self): self.assertIn("spot", state.attributes.get(ATTR_STATUS)) self.assertEqual(STATE_ON, state.state) + vacuum.start_pause(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.start_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) + self.hass.block_till_done() + state = self.hass.states.get(ENTITY_VACUUM_STATE) + self.assertEqual(STATE_IDLE, state.state) + + state = self.hass.states.get(ENTITY_VACUUM_STATE) + self.assertLess(state.attributes.get(ATTR_BATTERY_LEVEL), 100) + self.assertNotEqual(STATE_DOCKED, state.state) + + vacuum.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], + 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) + self.hass.block_till_done() + state = self.hass.states.get(ENTITY_VACUUM_STATE) + self.assertEqual(STATE_CLEANING, state.state) + def test_unsupported_methods(self): """Test service calls for unsupported vacuums.""" self.hass.states.set(ENTITY_VACUUM_NONE, STATE_ON) @@ -201,6 +247,22 @@ def test_unsupported_methods(self): self.assertNotIn("spot", state.attributes.get(ATTR_STATUS)) self.assertEqual(STATE_OFF, state.state) + # StateVacuumDevice does not support on/off + vacuum.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) + 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) + self.hass.block_till_done() + state = self.hass.states.get(ENTITY_VACUUM_STATE) + self.assertNotEqual(STATE_CLEANING, state.state) + def test_services(self): """Test vacuum services.""" # Test send_command @@ -241,9 +303,11 @@ def test_services(self): def test_set_fan_speed(self): """Test vacuum service to set the fan speed.""" group_vacuums = ','.join([ENTITY_VACUUM_BASIC, - ENTITY_VACUUM_COMPLETE]) + ENTITY_VACUUM_COMPLETE, + ENTITY_VACUUM_STATE]) old_state_basic = self.hass.states.get(ENTITY_VACUUM_BASIC) old_state_complete = self.hass.states.get(ENTITY_VACUUM_COMPLETE) + old_state_state = self.hass.states.get(ENTITY_VACUUM_STATE) vacuum.set_fan_speed( self.hass, FAN_SPEEDS[0], entity_id=group_vacuums) @@ -251,6 +315,7 @@ def test_set_fan_speed(self): self.hass.block_till_done() new_state_basic = self.hass.states.get(ENTITY_VACUUM_BASIC) new_state_complete = self.hass.states.get(ENTITY_VACUUM_COMPLETE) + new_state_state = self.hass.states.get(ENTITY_VACUUM_STATE) self.assertEqual(old_state_basic, new_state_basic) self.assertNotIn(ATTR_FAN_SPEED, new_state_basic.attributes) @@ -261,6 +326,12 @@ def test_set_fan_speed(self): self.assertEqual(FAN_SPEEDS[0], new_state_complete.attributes[ATTR_FAN_SPEED]) + self.assertNotEqual(old_state_state, new_state_state) + self.assertEqual(FAN_SPEEDS[1], + old_state_state.attributes[ATTR_FAN_SPEED]) + self.assertEqual(FAN_SPEEDS[0], + new_state_state.attributes[ATTR_FAN_SPEED]) + def test_send_command(self): """Test vacuum service to send a command.""" group_vacuums = ','.join([ENTITY_VACUUM_BASIC, From 2e5131bb2161b43461231a683e45d2c843d727f0 Mon Sep 17 00:00:00 2001 From: Niklas Date: Wed, 1 Aug 2018 17:07:27 +0200 Subject: [PATCH 019/117] Add support for STATE_AUTO of generic_thermostat (#15678) Add support for STATE_AUTO of generic_thermostat --- homeassistant/components/climate/generic_thermostat.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/climate/generic_thermostat.py b/homeassistant/components/climate/generic_thermostat.py index 3f1d9a208ac5fd..5e535bf6def57b 100644 --- a/homeassistant/components/climate/generic_thermostat.py +++ b/homeassistant/components/climate/generic_thermostat.py @@ -231,7 +231,14 @@ def operation_list(self): async def async_set_operation_mode(self, operation_mode): """Set operation mode.""" - if operation_mode == STATE_HEAT: + if operation_mode == STATE_AUTO: + if not self.ac_mode: + self._current_operation = STATE_HEAT + else: + self._current_operation = STATE_COOL + self._enabled = True + self._async_control_heating() + elif operation_mode == STATE_HEAT: self._current_operation = STATE_HEAT self._enabled = True self._async_control_heating() From 589b23b7e2a4467823a6d590e3c18b31677c42fe Mon Sep 17 00:00:00 2001 From: Jason Hu Date: Wed, 1 Aug 2018 10:04:41 -0700 Subject: [PATCH 020/117] Revert "Add support for STATE_AUTO of generic_thermostat (#15678)" (#15783) This reverts commit 2e5131bb2161b43461231a683e45d2c843d727f0. --- homeassistant/components/climate/generic_thermostat.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/homeassistant/components/climate/generic_thermostat.py b/homeassistant/components/climate/generic_thermostat.py index 5e535bf6def57b..3f1d9a208ac5fd 100644 --- a/homeassistant/components/climate/generic_thermostat.py +++ b/homeassistant/components/climate/generic_thermostat.py @@ -231,14 +231,7 @@ def operation_list(self): async def async_set_operation_mode(self, operation_mode): """Set operation mode.""" - if operation_mode == STATE_AUTO: - if not self.ac_mode: - self._current_operation = STATE_HEAT - else: - self._current_operation = STATE_COOL - self._enabled = True - self._async_control_heating() - elif operation_mode == STATE_HEAT: + if operation_mode == STATE_HEAT: self._current_operation = STATE_HEAT self._enabled = True self._async_control_heating() From 2f8d66ef2bd70339eb20b1048c2eb60a56660503 Mon Sep 17 00:00:00 2001 From: Wim Haanstra Date: Thu, 2 Aug 2018 07:01:40 +0200 Subject: [PATCH 021/117] RitAssist / FleetGO support (#15780) * RitAssist / FleetGO support * Fix lint issue Add to .coveragerc --- .coveragerc | 1 + .../components/device_tracker/ritassist.py | 87 +++++++++++++++++++ requirements_all.txt | 3 + 3 files changed, 91 insertions(+) create mode 100644 homeassistant/components/device_tracker/ritassist.py diff --git a/.coveragerc b/.coveragerc index 3d369eed073931..bce7205dcd798c 100644 --- a/.coveragerc +++ b/.coveragerc @@ -440,6 +440,7 @@ omit = homeassistant/components/device_tracker/netgear.py homeassistant/components/device_tracker/nmap_tracker.py homeassistant/components/device_tracker/ping.py + homeassistant/components/device_tracker/ritassist.py homeassistant/components/device_tracker/sky_hub.py homeassistant/components/device_tracker/snmp.py homeassistant/components/device_tracker/swisscom.py diff --git a/homeassistant/components/device_tracker/ritassist.py b/homeassistant/components/device_tracker/ritassist.py new file mode 100644 index 00000000000000..9fc50de5062329 --- /dev/null +++ b/homeassistant/components/device_tracker/ritassist.py @@ -0,0 +1,87 @@ +""" +Support for RitAssist Platform. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/device_tracker.ritassist/ +""" +import logging + +import requests +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.device_tracker import PLATFORM_SCHEMA +from homeassistant.const import CONF_USERNAME, CONF_PASSWORD +from homeassistant.helpers.event import track_utc_time_change + +REQUIREMENTS = ['ritassist==0.5'] + +_LOGGER = logging.getLogger(__name__) + +CONF_CLIENT_ID = 'client_id' +CONF_CLIENT_SECRET = 'client_secret' +CONF_INCLUDE = 'include' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Required(CONF_CLIENT_ID): cv.string, + vol.Required(CONF_CLIENT_SECRET): cv.string, + vol.Optional(CONF_INCLUDE, default=[]): + vol.All(cv.ensure_list, [cv.string]) +}) + + +def setup_scanner(hass, config: dict, see, discovery_info=None): + """Set up the DeviceScanner and check if login is valid.""" + scanner = RitAssistDeviceScanner(config, see) + if not scanner.login(hass): + _LOGGER.error('RitAssist authentication failed') + return False + return True + + +class RitAssistDeviceScanner: + """Define a scanner for the RitAssist platform.""" + + def __init__(self, config, see): + """Initialize RitAssistDeviceScanner.""" + from ritassist import API + + self._include = config.get(CONF_INCLUDE) + self._see = see + + self._api = API(config.get(CONF_CLIENT_ID), + config.get(CONF_CLIENT_SECRET), + config.get(CONF_USERNAME), + config.get(CONF_PASSWORD)) + + def setup(self, hass): + """Setup a timer and start gathering devices.""" + self._refresh() + track_utc_time_change(hass, + lambda now: self._refresh(), + second=range(0, 60, 30)) + + def login(self, hass): + """Perform a login on the RitAssist API.""" + if self._api.login(): + self.setup(hass) + return True + return False + + def _refresh(self) -> None: + """Refresh device information from the platform.""" + try: + devices = self._api.get_devices() + + for device in devices: + if (not self._include or + device.license_plate in self._include): + self._see(dev_id=device.plate_as_id, + gps=(device.latitude, device.longitude), + attributes=device.state_attributes, + icon='mdi:car') + + except requests.exceptions.ConnectionError: + _LOGGER.error('ConnectionError: Could not connect to RitAssist') diff --git a/requirements_all.txt b/requirements_all.txt index 6388842e84d94c..b0f2172834e8f7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1206,6 +1206,9 @@ rflink==0.0.37 # homeassistant.components.ring ring_doorbell==0.2.1 +# homeassistant.components.device_tracker.ritassist +ritassist==0.5 + # homeassistant.components.notify.rocketchat rocketchat-API==0.6.1 From bdea9e1333afbd849ffb51b65461d10d9590590f Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Wed, 1 Aug 2018 23:42:12 -0600 Subject: [PATCH 022/117] Add support for OpenUV binary sensors and sensors (#15769) * Initial commit * Adjusted ownership and coverage * Member-requested changes * Updated Ozone to a value, not an index * Verbiage update --- .coveragerc | 3 + CODEOWNERS | 2 + .../components/binary_sensor/openuv.py | 103 ++++++++++ homeassistant/components/openuv.py | 182 ++++++++++++++++++ homeassistant/components/sensor/openuv.py | 121 ++++++++++++ requirements_all.txt | 3 + 6 files changed, 414 insertions(+) create mode 100644 homeassistant/components/binary_sensor/openuv.py create mode 100644 homeassistant/components/openuv.py create mode 100644 homeassistant/components/sensor/openuv.py diff --git a/.coveragerc b/.coveragerc index bce7205dcd798c..5dd2c66b56eca2 100644 --- a/.coveragerc +++ b/.coveragerc @@ -215,6 +215,9 @@ omit = homeassistant/components/opencv.py homeassistant/components/*/opencv.py + homeassistant/components/openuv.py + homeassistant/components/*/openuv.py + homeassistant/components/pilight.py homeassistant/components/*/pilight.py diff --git a/CODEOWNERS b/CODEOWNERS index 556791b879c64e..53f577d02ebe80 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -98,6 +98,8 @@ 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.py @bachya homeassistant/components/qwikswitch.py @kellerza homeassistant/components/*/qwikswitch.py @kellerza homeassistant/components/rainmachine/* @bachya diff --git a/homeassistant/components/binary_sensor/openuv.py b/homeassistant/components/binary_sensor/openuv.py new file mode 100644 index 00000000000000..3a2732d3be037b --- /dev/null +++ b/homeassistant/components/binary_sensor/openuv.py @@ -0,0 +1,103 @@ +""" +This platform provides binary sensors for OpenUV data. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/binary_sensor.openuv/ +""" +import logging + +from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.const import CONF_MONITORED_CONDITIONS +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.components.openuv import ( + BINARY_SENSORS, DATA_PROTECTION_WINDOW, DOMAIN, TOPIC_UPDATE, + TYPE_PROTECTION_WINDOW, OpenUvEntity) +from homeassistant.util.dt import as_local, parse_datetime, utcnow + +DEPENDENCIES = ['openuv'] +_LOGGER = logging.getLogger(__name__) + +ATTR_PROTECTION_WINDOW_STARTING_TIME = 'start_time' +ATTR_PROTECTION_WINDOW_STARTING_UV = 'start_uv' +ATTR_PROTECTION_WINDOW_ENDING_TIME = 'end_time' +ATTR_PROTECTION_WINDOW_ENDING_UV = 'end_uv' + + +async def async_setup_platform( + hass, config, async_add_devices, discovery_info=None): + """Set up the OpenUV binary sensor platform.""" + if discovery_info is None: + return + + openuv = hass.data[DOMAIN] + + binary_sensors = [] + for sensor_type in discovery_info[CONF_MONITORED_CONDITIONS]: + name, icon = BINARY_SENSORS[sensor_type] + binary_sensors.append( + OpenUvBinarySensor(openuv, sensor_type, name, icon)) + + async_add_devices(binary_sensors, True) + + +class OpenUvBinarySensor(OpenUvEntity, BinarySensorDevice): + """Define a binary sensor for OpenUV.""" + + def __init__(self, openuv, sensor_type, name, icon): + """Initialize the sensor.""" + super().__init__(openuv) + + self._icon = icon + self._latitude = openuv.client.latitude + self._longitude = openuv.client.longitude + self._name = name + self._sensor_type = sensor_type + self._state = None + + @property + def icon(self): + """Return the icon.""" + return self._icon + + @property + def is_on(self): + """Return the status of the sensor.""" + return self._state + + @property + def should_poll(self): + """Disable polling.""" + return False + + @property + def unique_id(self) -> str: + """Return a unique, HASS-friendly identifier for this entity.""" + return '{0}_{1}_{2}'.format( + self._latitude, self._longitude, self._sensor_type) + + @callback + def _update_data(self): + """Update the state.""" + self.async_schedule_update_ha_state(True) + + async def async_added_to_hass(self): + """Register callbacks.""" + async_dispatcher_connect( + self.hass, TOPIC_UPDATE, self._update_data) + + async def async_update(self): + """Update the state.""" + data = self.openuv.data[DATA_PROTECTION_WINDOW]['result'] + if self._sensor_type == TYPE_PROTECTION_WINDOW: + self._state = parse_datetime( + data['from_time']) <= utcnow() <= parse_datetime( + data['to_time']) + self._attrs.update({ + ATTR_PROTECTION_WINDOW_ENDING_TIME: + as_local(parse_datetime(data['to_time'])), + ATTR_PROTECTION_WINDOW_ENDING_UV: data['to_uv'], + ATTR_PROTECTION_WINDOW_STARTING_UV: data['from_uv'], + ATTR_PROTECTION_WINDOW_STARTING_TIME: + as_local(parse_datetime(data['from_time'])), + }) diff --git a/homeassistant/components/openuv.py b/homeassistant/components/openuv.py new file mode 100644 index 00000000000000..dd038611ae96dc --- /dev/null +++ b/homeassistant/components/openuv.py @@ -0,0 +1,182 @@ +""" +Support for data from openuv.io. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/openuv/ +""" +import logging +from datetime import timedelta + +import voluptuous as vol + +from homeassistant.const import ( + ATTR_ATTRIBUTION, CONF_API_KEY, CONF_BINARY_SENSORS, CONF_ELEVATION, + CONF_LATITUDE, CONF_LONGITUDE, CONF_MONITORED_CONDITIONS, + CONF_SCAN_INTERVAL, CONF_SENSORS) +from homeassistant.helpers import ( + aiohttp_client, config_validation as cv, discovery) +from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.event import async_track_time_interval + +REQUIREMENTS = ['pyopenuv==1.0.1'] +_LOGGER = logging.getLogger(__name__) + +DOMAIN = 'openuv' + +DATA_PROTECTION_WINDOW = 'protection_window' +DATA_UV = 'uv' + +DEFAULT_ATTRIBUTION = 'Data provided by OpenUV' +DEFAULT_SCAN_INTERVAL = timedelta(minutes=30) + +NOTIFICATION_ID = 'openuv_notification' +NOTIFICATION_TITLE = 'OpenUV Component Setup' + +TOPIC_UPDATE = '{0}_data_update'.format(DOMAIN) + +TYPE_CURRENT_OZONE_LEVEL = 'current_ozone_level' +TYPE_CURRENT_UV_INDEX = 'current_uv_index' +TYPE_MAX_UV_INDEX = 'max_uv_index' +TYPE_PROTECTION_WINDOW = 'uv_protection_window' +TYPE_SAFE_EXPOSURE_TIME_1 = 'safe_exposure_time_type_1' +TYPE_SAFE_EXPOSURE_TIME_2 = 'safe_exposure_time_type_2' +TYPE_SAFE_EXPOSURE_TIME_3 = 'safe_exposure_time_type_3' +TYPE_SAFE_EXPOSURE_TIME_4 = 'safe_exposure_time_type_4' +TYPE_SAFE_EXPOSURE_TIME_5 = 'safe_exposure_time_type_5' +TYPE_SAFE_EXPOSURE_TIME_6 = 'safe_exposure_time_type_6' + +BINARY_SENSORS = { + TYPE_PROTECTION_WINDOW: ('Protection Window', 'mdi:sunglasses') +} + +BINARY_SENSOR_SCHEMA = vol.Schema({ + vol.Optional(CONF_MONITORED_CONDITIONS, default=list(BINARY_SENSORS)): + vol.All(cv.ensure_list, [vol.In(BINARY_SENSORS)]) +}) + +SENSORS = { + TYPE_CURRENT_OZONE_LEVEL: ( + 'Current Ozone Level', 'mdi:vector-triangle', 'du'), + TYPE_CURRENT_UV_INDEX: ('Current UV Index', 'mdi:weather-sunny', 'index'), + TYPE_MAX_UV_INDEX: ('Max UV Index', 'mdi:weather-sunny', 'index'), + TYPE_SAFE_EXPOSURE_TIME_1: ( + 'Skin Type 1 Safe Exposure Time', 'mdi:timer', 'minutes'), + TYPE_SAFE_EXPOSURE_TIME_2: ( + 'Skin Type 2 Safe Exposure Time', 'mdi:timer', 'minutes'), + TYPE_SAFE_EXPOSURE_TIME_3: ( + 'Skin Type 3 Safe Exposure Time', 'mdi:timer', 'minutes'), + TYPE_SAFE_EXPOSURE_TIME_4: ( + 'Skin Type 4 Safe Exposure Time', 'mdi:timer', 'minutes'), + TYPE_SAFE_EXPOSURE_TIME_5: ( + 'Skin Type 5 Safe Exposure Time', 'mdi:timer', 'minutes'), + TYPE_SAFE_EXPOSURE_TIME_6: ( + 'Skin Type 6 Safe Exposure Time', 'mdi:timer', 'minutes'), +} + +SENSOR_SCHEMA = vol.Schema({ + vol.Optional(CONF_MONITORED_CONDITIONS, default=list(SENSORS)): + vol.All(cv.ensure_list, [vol.In(SENSORS)]) +}) + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_API_KEY): cv.string, + vol.Optional(CONF_ELEVATION): float, + vol.Optional(CONF_LATITUDE): cv.latitude, + vol.Optional(CONF_LONGITUDE): cv.longitude, + vol.Optional(CONF_BINARY_SENSORS, default={}): BINARY_SENSOR_SCHEMA, + vol.Optional(CONF_SENSORS, default={}): SENSOR_SCHEMA, + vol.Optional(CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL): + cv.time_period, + }) +}, extra=vol.ALLOW_EXTRA) + + +async def async_setup(hass, config): + """Set up the OpenUV component.""" + from pyopenuv import Client + from pyopenuv.errors import OpenUvError + + conf = config[DOMAIN] + api_key = conf[CONF_API_KEY] + elevation = conf.get(CONF_ELEVATION, hass.config.elevation) + latitude = conf.get(CONF_LATITUDE, hass.config.latitude) + longitude = conf.get(CONF_LONGITUDE, hass.config.longitude) + + try: + websession = aiohttp_client.async_get_clientsession(hass) + openuv = OpenUV( + Client( + api_key, latitude, longitude, websession, altitude=elevation), + conf[CONF_BINARY_SENSORS][CONF_MONITORED_CONDITIONS] + + conf[CONF_SENSORS][CONF_MONITORED_CONDITIONS]) + await openuv.async_update() + hass.data[DOMAIN] = openuv + except OpenUvError as err: + _LOGGER.error('An error occurred: %s', str(err)) + hass.components.persistent_notification.create( + 'Error: {0}
' + 'You will need to restart hass after fixing.' + ''.format(err), + title=NOTIFICATION_TITLE, + notification_id=NOTIFICATION_ID) + return False + + for component, schema in [ + ('binary_sensor', conf[CONF_BINARY_SENSORS]), + ('sensor', conf[CONF_SENSORS]), + ]: + hass.async_create_task( + discovery.async_load_platform( + hass, component, DOMAIN, schema, config)) + + async def refresh_sensors(event_time): + """Refresh OpenUV data.""" + _LOGGER.debug('Refreshing OpenUV data') + await openuv.async_update() + async_dispatcher_send(hass, TOPIC_UPDATE) + + async_track_time_interval(hass, refresh_sensors, conf[CONF_SCAN_INTERVAL]) + + return True + + +class OpenUV: + """Define a generic OpenUV object.""" + + def __init__(self, client, monitored_conditions): + """Initialize.""" + self._monitored_conditions = monitored_conditions + self.client = client + self.data = {} + + async def async_update(self): + """Update sensor/binary sensor data.""" + if TYPE_PROTECTION_WINDOW in self._monitored_conditions: + data = await self.client.uv_protection_window() + self.data[DATA_PROTECTION_WINDOW] = data + + if any(c in self._monitored_conditions for c in SENSORS): + data = await self.client.uv_index() + self.data[DATA_UV] = data + + +class OpenUvEntity(Entity): + """Define a generic OpenUV entity.""" + + def __init__(self, openuv): + """Initialize.""" + self._attrs = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION} + self._name = None + self.openuv = openuv + + @property + def device_state_attributes(self): + """Return the state attributes.""" + return self._attrs + + @property + def name(self): + """Return the name of the entity.""" + return self._name diff --git a/homeassistant/components/sensor/openuv.py b/homeassistant/components/sensor/openuv.py new file mode 100644 index 00000000000000..b30c2908c40b7c --- /dev/null +++ b/homeassistant/components/sensor/openuv.py @@ -0,0 +1,121 @@ +""" +This platform provides sensors for OpenUV data. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.openuv/ +""" +import logging + +from homeassistant.const import CONF_MONITORED_CONDITIONS +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.components.openuv import ( + DATA_UV, DOMAIN, SENSORS, TOPIC_UPDATE, TYPE_CURRENT_OZONE_LEVEL, + TYPE_CURRENT_UV_INDEX, TYPE_MAX_UV_INDEX, TYPE_SAFE_EXPOSURE_TIME_1, + TYPE_SAFE_EXPOSURE_TIME_2, TYPE_SAFE_EXPOSURE_TIME_3, + TYPE_SAFE_EXPOSURE_TIME_4, TYPE_SAFE_EXPOSURE_TIME_5, + TYPE_SAFE_EXPOSURE_TIME_6, OpenUvEntity) +from homeassistant.util.dt import as_local, parse_datetime + +DEPENDENCIES = ['openuv'] +_LOGGER = logging.getLogger(__name__) + +ATTR_MAX_UV_TIME = 'time' + +EXPOSURE_TYPE_MAP = { + TYPE_SAFE_EXPOSURE_TIME_1: 'st1', + TYPE_SAFE_EXPOSURE_TIME_2: 'st2', + TYPE_SAFE_EXPOSURE_TIME_3: 'st3', + TYPE_SAFE_EXPOSURE_TIME_4: 'st4', + TYPE_SAFE_EXPOSURE_TIME_5: 'st5', + TYPE_SAFE_EXPOSURE_TIME_6: 'st6' +} + + +async def async_setup_platform( + hass, config, async_add_devices, discovery_info=None): + """Set up the OpenUV binary sensor platform.""" + if discovery_info is None: + return + + openuv = hass.data[DOMAIN] + + sensors = [] + for sensor_type in discovery_info[CONF_MONITORED_CONDITIONS]: + name, icon, unit = SENSORS[sensor_type] + sensors.append(OpenUvSensor(openuv, sensor_type, name, icon, unit)) + + async_add_devices(sensors, True) + + +class OpenUvSensor(OpenUvEntity): + """Define a binary sensor for OpenUV.""" + + def __init__(self, openuv, sensor_type, name, icon, unit): + """Initialize the sensor.""" + super().__init__(openuv) + + self._icon = icon + self._latitude = openuv.client.latitude + self._longitude = openuv.client.longitude + self._name = name + self._sensor_type = sensor_type + self._state = None + self._unit = unit + + @property + def icon(self): + """Return the icon.""" + return self._icon + + @property + def should_poll(self): + """Disable polling.""" + return False + + @property + def state(self): + """Return the status of the sensor.""" + return self._state + + @property + def unique_id(self) -> str: + """Return a unique, HASS-friendly identifier for this entity.""" + return '{0}_{1}_{2}'.format( + self._latitude, self._longitude, self._sensor_type) + + @property + def unit_of_measurement(self): + """Return the unit the value is expressed in.""" + return self._unit + + @callback + def _update_data(self): + """Update the state.""" + self.async_schedule_update_ha_state(True) + + async def async_added_to_hass(self): + """Register callbacks.""" + async_dispatcher_connect(self.hass, TOPIC_UPDATE, self._update_data) + + async def async_update(self): + """Update the state.""" + data = self.openuv.data[DATA_UV]['result'] + if self._sensor_type == TYPE_CURRENT_OZONE_LEVEL: + self._state = data['ozone'] + elif self._sensor_type == TYPE_CURRENT_UV_INDEX: + self._state = data['uv'] + elif self._sensor_type == TYPE_MAX_UV_INDEX: + self._state = data['uv_max'] + self._attrs.update({ + ATTR_MAX_UV_TIME: as_local( + parse_datetime(data['uv_max_time'])) + }) + elif self._sensor_type in (TYPE_SAFE_EXPOSURE_TIME_1, + TYPE_SAFE_EXPOSURE_TIME_2, + TYPE_SAFE_EXPOSURE_TIME_3, + TYPE_SAFE_EXPOSURE_TIME_4, + TYPE_SAFE_EXPOSURE_TIME_5, + TYPE_SAFE_EXPOSURE_TIME_6): + self._state = data['safe_exposure_time'][EXPOSURE_TYPE_MAP[ + self._sensor_type]] diff --git a/requirements_all.txt b/requirements_all.txt index b0f2172834e8f7..aa619142e0174a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -962,6 +962,9 @@ pynut2==2.1.2 # homeassistant.components.binary_sensor.nx584 pynx584==0.4 +# homeassistant.components.openuv +pyopenuv==1.0.1 + # homeassistant.components.iota pyota==2.0.5 From 7972d6a0c63faaba179dc5f980fb4b375b397ac7 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 2 Aug 2018 13:42:45 +0200 Subject: [PATCH 023/117] Update translations --- .../components/cast/.translations/de.json | 7 ++- .../components/cast/.translations/es-419.json | 15 +++++ .../components/cast/.translations/ja.json | 14 +++++ .../components/cast/.translations/ko.json | 2 +- .../components/cast/.translations/pt-BR.json | 15 +++++ .../components/cast/.translations/pt.json | 15 +++++ .../cast/.translations/zh-Hans.json | 2 +- .../deconz/.translations/es-419.json | 31 ++++++++++ .../components/deconz/.translations/it.json | 7 +++ .../components/deconz/.translations/ja.json | 15 +++++ .../components/deconz/.translations/ko.json | 2 +- .../components/deconz/.translations/pl.json | 3 +- .../deconz/.translations/pt-BR.json | 3 +- .../components/deconz/.translations/pt.json | 3 +- .../homematicip_cloud/.translations/ca.json | 30 ++++++++++ .../homematicip_cloud/.translations/cs.json | 30 ++++++++++ .../homematicip_cloud/.translations/de.json | 28 ++++++++++ .../homematicip_cloud/.translations/en.json | 56 +++++++++---------- .../.translations/es-419.json | 23 ++++++++ .../homematicip_cloud/.translations/hu.json | 5 ++ .../homematicip_cloud/.translations/ja.json | 13 +++++ .../homematicip_cloud/.translations/ko.json | 30 ++++++++++ .../homematicip_cloud/.translations/lb.json | 30 ++++++++++ .../homematicip_cloud/.translations/nl.json | 30 ++++++++++ .../homematicip_cloud/.translations/no.json | 30 ++++++++++ .../homematicip_cloud/.translations/pl.json | 30 ++++++++++ .../.translations/pt-BR.json | 30 ++++++++++ .../homematicip_cloud/.translations/pt.json | 30 ++++++++++ .../homematicip_cloud/.translations/ru.json | 30 ++++++++++ .../homematicip_cloud/.translations/sl.json | 30 ++++++++++ .../homematicip_cloud/.translations/sv.json | 30 ++++++++++ .../.translations/zh-Hans.json | 30 ++++++++++ .../.translations/zh-Hant.json | 30 ++++++++++ .../components/hue/.translations/es-419.json | 24 ++++++++ .../components/hue/.translations/ja.json | 15 +++++ .../components/nest/.translations/de.json | 12 +++- .../components/nest/.translations/es-419.json | 27 +++++++++ .../components/nest/.translations/it.json | 16 ++++++ .../components/nest/.translations/ja.json | 5 ++ .../components/nest/.translations/pt-BR.json | 33 +++++++++++ .../components/nest/.translations/pt.json | 33 +++++++++++ .../sensor/.translations/season.es-419.json | 8 +++ .../sensor/.translations/season.lv.json | 8 +++ .../components/sonos/.translations/de.json | 7 ++- .../sonos/.translations/es-419.json | 15 +++++ .../components/sonos/.translations/ko.json | 2 +- .../components/sonos/.translations/pt-BR.json | 15 +++++ .../components/sonos/.translations/pt.json | 15 +++++ .../components/zone/.translations/es-419.json | 21 +++++++ .../components/zone/.translations/ja.json | 13 +++++ 50 files changed, 906 insertions(+), 42 deletions(-) create mode 100644 homeassistant/components/cast/.translations/es-419.json create mode 100644 homeassistant/components/cast/.translations/ja.json create mode 100644 homeassistant/components/cast/.translations/pt-BR.json create mode 100644 homeassistant/components/cast/.translations/pt.json create mode 100644 homeassistant/components/deconz/.translations/es-419.json create mode 100644 homeassistant/components/deconz/.translations/ja.json create mode 100644 homeassistant/components/homematicip_cloud/.translations/ca.json create mode 100644 homeassistant/components/homematicip_cloud/.translations/cs.json create mode 100644 homeassistant/components/homematicip_cloud/.translations/de.json create mode 100644 homeassistant/components/homematicip_cloud/.translations/es-419.json create mode 100644 homeassistant/components/homematicip_cloud/.translations/hu.json create mode 100644 homeassistant/components/homematicip_cloud/.translations/ja.json create mode 100644 homeassistant/components/homematicip_cloud/.translations/ko.json create mode 100644 homeassistant/components/homematicip_cloud/.translations/lb.json create mode 100644 homeassistant/components/homematicip_cloud/.translations/nl.json create mode 100644 homeassistant/components/homematicip_cloud/.translations/no.json create mode 100644 homeassistant/components/homematicip_cloud/.translations/pl.json create mode 100644 homeassistant/components/homematicip_cloud/.translations/pt-BR.json create mode 100644 homeassistant/components/homematicip_cloud/.translations/pt.json create mode 100644 homeassistant/components/homematicip_cloud/.translations/ru.json create mode 100644 homeassistant/components/homematicip_cloud/.translations/sl.json create mode 100644 homeassistant/components/homematicip_cloud/.translations/sv.json create mode 100644 homeassistant/components/homematicip_cloud/.translations/zh-Hans.json create mode 100644 homeassistant/components/homematicip_cloud/.translations/zh-Hant.json create mode 100644 homeassistant/components/hue/.translations/es-419.json create mode 100644 homeassistant/components/hue/.translations/ja.json create mode 100644 homeassistant/components/nest/.translations/es-419.json create mode 100644 homeassistant/components/nest/.translations/ja.json create mode 100644 homeassistant/components/nest/.translations/pt-BR.json create mode 100644 homeassistant/components/nest/.translations/pt.json create mode 100644 homeassistant/components/sensor/.translations/season.es-419.json create mode 100644 homeassistant/components/sensor/.translations/season.lv.json create mode 100644 homeassistant/components/sonos/.translations/es-419.json create mode 100644 homeassistant/components/sonos/.translations/pt-BR.json create mode 100644 homeassistant/components/sonos/.translations/pt.json create mode 100644 homeassistant/components/zone/.translations/es-419.json create mode 100644 homeassistant/components/zone/.translations/ja.json diff --git a/homeassistant/components/cast/.translations/de.json b/homeassistant/components/cast/.translations/de.json index 2572c3344ebac2..a37dbd6f5b7fc1 100644 --- a/homeassistant/components/cast/.translations/de.json +++ b/homeassistant/components/cast/.translations/de.json @@ -1,14 +1,15 @@ { "config": { "abort": { - "no_devices_found": "Keine Google Cast Ger\u00e4te im Netzwerk gefunden." + "no_devices_found": "Keine Google Cast Ger\u00e4te im Netzwerk gefunden.", + "single_instance_allowed": "Nur eine einzige Konfiguration von Google Cast ist notwendig." }, "step": { "confirm": { "description": "M\u00f6chten Sie Google Cast einrichten?", - "title": "" + "title": "Google Cast" } }, - "title": "" + "title": "Google Cast" } } \ No newline at end of file diff --git a/homeassistant/components/cast/.translations/es-419.json b/homeassistant/components/cast/.translations/es-419.json new file mode 100644 index 00000000000000..2f8d4982afdd0e --- /dev/null +++ b/homeassistant/components/cast/.translations/es-419.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "No se encontraron dispositivos Google Cast en la red.", + "single_instance_allowed": "S\u00f3lo es necesaria una \u00fanica configuraci\u00f3n de Google Cast." + }, + "step": { + "confirm": { + "description": "\u00bfDesea configurar Google Cast?", + "title": "Google Cast" + } + }, + "title": "Google Cast" + } +} \ No newline at end of file diff --git a/homeassistant/components/cast/.translations/ja.json b/homeassistant/components/cast/.translations/ja.json new file mode 100644 index 00000000000000..25b9c10b2e7435 --- /dev/null +++ b/homeassistant/components/cast/.translations/ja.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306bGoogle Cast\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3067\u3057\u305f\u3002" + }, + "step": { + "confirm": { + "description": "Google Cast\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f", + "title": "Google Cast" + } + }, + "title": "Google Cast" + } +} \ No newline at end of file diff --git a/homeassistant/components/cast/.translations/ko.json b/homeassistant/components/cast/.translations/ko.json index 2be2a69c171327..e4472c88cd8e3a 100644 --- a/homeassistant/components/cast/.translations/ko.json +++ b/homeassistant/components/cast/.translations/ko.json @@ -2,7 +2,7 @@ "config": { "abort": { "no_devices_found": "Googgle Cast \uc7a5\uce58\uac00 \ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \ubc1c\uacac\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4.", - "single_instance_allowed": "Google Cast\uc758 \ub2e8\uc77c \uad6c\uc131 \ub9cc \ud544\uc694\ud569\ub2c8\ub2e4." + "single_instance_allowed": "\ud558\ub098\uc758 Google Cast \ub9cc \uad6c\uc131 \ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4." }, "step": { "confirm": { diff --git a/homeassistant/components/cast/.translations/pt-BR.json b/homeassistant/components/cast/.translations/pt-BR.json new file mode 100644 index 00000000000000..bd670d7c72f56e --- /dev/null +++ b/homeassistant/components/cast/.translations/pt-BR.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Nenhum dispositivo Google Cast encontrado na rede.", + "single_instance_allowed": "Apenas uma \u00fanica configura\u00e7\u00e3o do Google Cast \u00e9 necess\u00e1ria." + }, + "step": { + "confirm": { + "description": "Deseja configurar o Google Cast?", + "title": "" + } + }, + "title": "" + } +} \ No newline at end of file diff --git a/homeassistant/components/cast/.translations/pt.json b/homeassistant/components/cast/.translations/pt.json new file mode 100644 index 00000000000000..a6d28538396882 --- /dev/null +++ b/homeassistant/components/cast/.translations/pt.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Nenhum dispositivo Google Cast descoberto na rede.", + "single_instance_allowed": "Apenas uma \u00fanica configura\u00e7\u00e3o do Google Cast \u00e9 necess\u00e1ria." + }, + "step": { + "confirm": { + "description": "Deseja configurar o Google Cast?", + "title": "" + } + }, + "title": "" + } +} \ No newline at end of file diff --git a/homeassistant/components/cast/.translations/zh-Hans.json b/homeassistant/components/cast/.translations/zh-Hans.json index 4a844d3d4dd84a..d4f1cf4c1a5907 100644 --- a/homeassistant/components/cast/.translations/zh-Hans.json +++ b/homeassistant/components/cast/.translations/zh-Hans.json @@ -2,7 +2,7 @@ "config": { "abort": { "no_devices_found": "\u6ca1\u6709\u5728\u7f51\u7edc\u4e0a\u627e\u5230 Google Cast \u8bbe\u5907\u3002", - "single_instance_allowed": "\u53ea\u6709\u4e00\u6b21 Google Cast \u914d\u7f6e\u662f\u5fc5\u8981\u7684\u3002" + "single_instance_allowed": "Google Cast \u53ea\u9700\u8981\u914d\u7f6e\u4e00\u6b21\u3002" }, "step": { "confirm": { diff --git a/homeassistant/components/deconz/.translations/es-419.json b/homeassistant/components/deconz/.translations/es-419.json new file mode 100644 index 00000000000000..ab47a5b43c824c --- /dev/null +++ b/homeassistant/components/deconz/.translations/es-419.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "El Bridge ya est\u00e1 configurado", + "no_bridges": "No se descubrieron puentes deCONZ", + "one_instance_only": "El componente solo admite una instancia deCONZ" + }, + "error": { + "no_key": "No se pudo obtener una clave de API" + }, + "step": { + "init": { + "data": { + "host": "Host", + "port": "Puerto (valor predeterminado: '80')" + }, + "title": "Definir el gateway deCONZ" + }, + "link": { + "title": "Enlazar con deCONZ" + }, + "options": { + "data": { + "allow_clip_sensor": "Permitir la importaci\u00f3n de sensores virtuales", + "allow_deconz_groups": "Permitir la importaci\u00f3n de grupos deCONZ" + } + } + }, + "title": "deCONZ Zigbee gateway" + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/it.json b/homeassistant/components/deconz/.translations/it.json index 6fc7158b88269c..87dcd0610f2ccf 100644 --- a/homeassistant/components/deconz/.translations/it.json +++ b/homeassistant/components/deconz/.translations/it.json @@ -19,6 +19,13 @@ "link": { "description": "Sblocca il tuo gateway deCONZ per registrarlo in Home Assistant.\n\n1. Vai nelle impostazioni di sistema di deCONZ\n2. Premi il bottone \"Unlock Gateway\"", "title": "Collega con deCONZ" + }, + "options": { + "data": { + "allow_clip_sensor": "Consenti l'importazione di sensori virtuali", + "allow_deconz_groups": "Consenti l'importazione di gruppi deCONZ" + }, + "title": "Opzioni di configurazione extra per deCONZ" } }, "title": "deCONZ" diff --git a/homeassistant/components/deconz/.translations/ja.json b/homeassistant/components/deconz/.translations/ja.json new file mode 100644 index 00000000000000..5148ebeaa86b2c --- /dev/null +++ b/homeassistant/components/deconz/.translations/ja.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "no_key": "API\u30ad\u30fc\u3092\u53d6\u5f97\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f" + }, + "step": { + "init": { + "data": { + "host": "\u30db\u30b9\u30c8", + "port": "\u30dd\u30fc\u30c8\uff08\u30c7\u30d5\u30a9\u30eb\u30c8\u5024\uff1a'80'\uff09" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/ko.json b/homeassistant/components/deconz/.translations/ko.json index 9c5ffa19257f3b..a584a1db9b50e2 100644 --- a/homeassistant/components/deconz/.translations/ko.json +++ b/homeassistant/components/deconz/.translations/ko.json @@ -23,7 +23,7 @@ "options": { "data": { "allow_clip_sensor": "\uac00\uc0c1 \uc13c\uc11c \uac00\uc838\uc624\uae30 \ud5c8\uc6a9", - "allow_deconz_groups": "deCONZ \ub0b4\uc6a9 \uac00\uc838\uc624\uae30 \ud5c8\uc6a9" + "allow_deconz_groups": "deCONZ \uadf8\ub8f9 \uac00\uc838\uc624\uae30 \ud5c8\uc6a9" }, "title": "deCONZ\ub97c \uc704\ud55c \ucd94\uac00 \uad6c\uc131 \uc635\uc158" } diff --git a/homeassistant/components/deconz/.translations/pl.json b/homeassistant/components/deconz/.translations/pl.json index 461e8b185eebeb..5dd87d9e46214a 100644 --- a/homeassistant/components/deconz/.translations/pl.json +++ b/homeassistant/components/deconz/.translations/pl.json @@ -22,7 +22,8 @@ }, "options": { "data": { - "allow_clip_sensor": "Zezwalaj na importowanie wirtualnych sensor\u00f3w" + "allow_clip_sensor": "Zezwalaj na importowanie wirtualnych sensor\u00f3w", + "allow_deconz_groups": "Zezw\u00f3l na importowanie grup deCONZ" }, "title": "Dodatkowe opcje konfiguracji dla deCONZ" } diff --git a/homeassistant/components/deconz/.translations/pt-BR.json b/homeassistant/components/deconz/.translations/pt-BR.json index 065c51aee21cdc..be79e7e461ae0b 100644 --- a/homeassistant/components/deconz/.translations/pt-BR.json +++ b/homeassistant/components/deconz/.translations/pt-BR.json @@ -22,7 +22,8 @@ }, "options": { "data": { - "allow_clip_sensor": "Permitir a importa\u00e7\u00e3o de sensores virtuais" + "allow_clip_sensor": "Permitir a importa\u00e7\u00e3o de sensores virtuais", + "allow_deconz_groups": "Permitir a importa\u00e7\u00e3o de grupos deCONZ" }, "title": "Op\u00e7\u00f5es extras de configura\u00e7\u00e3o para deCONZ" } diff --git a/homeassistant/components/deconz/.translations/pt.json b/homeassistant/components/deconz/.translations/pt.json index 6ccbfe9f217d56..1f7b8209089e6b 100644 --- a/homeassistant/components/deconz/.translations/pt.json +++ b/homeassistant/components/deconz/.translations/pt.json @@ -22,7 +22,8 @@ }, "options": { "data": { - "allow_clip_sensor": "Permitir a importa\u00e7\u00e3o de sensores virtuais" + "allow_clip_sensor": "Permitir a importa\u00e7\u00e3o de sensores virtuais", + "allow_deconz_groups": "Permitir a importa\u00e7\u00e3o de grupos deCONZ" }, "title": "Op\u00e7\u00f5es extra de configura\u00e7\u00e3o para deCONZ" } diff --git a/homeassistant/components/homematicip_cloud/.translations/ca.json b/homeassistant/components/homematicip_cloud/.translations/ca.json new file mode 100644 index 00000000000000..9d40bc2d24170d --- /dev/null +++ b/homeassistant/components/homematicip_cloud/.translations/ca.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "El punt d'acc\u00e9s ja est\u00e0 configurat", + "conection_aborted": "No s'ha pogut connectar al servidor HMIP", + "unknown": "S'ha produ\u00eft un error desconegut." + }, + "error": { + "invalid_pin": "Codi PIN inv\u00e0lid, torna-ho a provar.", + "press_the_button": "Si us plau, premeu el bot\u00f3 blau.", + "register_failed": "Error al registrar, torneu-ho a provar.", + "timeout_button": "Temps d'espera per pr\u00e9mer el bot\u00f3 blau esgotat, torneu-ho a provar." + }, + "step": { + "init": { + "data": { + "hapid": "Identificador del punt d'acc\u00e9s (SGTIN)", + "name": "Nom (opcional, s'utilitza com a nom prefix per a tots els dispositius)", + "pin": "Codi PIN (opcional)" + }, + "title": "Trieu el punt d'acc\u00e9s HomematicIP" + }, + "link": { + "description": "Premeu el bot\u00f3 blau del punt d'acc\u00e9s i el bot\u00f3 de enviar per registrar HomematicIP amb Home Assistent. \n\n ![Ubicaci\u00f3 del bot\u00f3 al pont](/static/images/config_flows/config_homematicip_cloud.png)", + "title": "Enlla\u00e7ar punt d'acc\u00e9s" + } + }, + "title": "HomematicIP Cloud" + } +} \ No newline at end of file diff --git a/homeassistant/components/homematicip_cloud/.translations/cs.json b/homeassistant/components/homematicip_cloud/.translations/cs.json new file mode 100644 index 00000000000000..59f232edea486a --- /dev/null +++ b/homeassistant/components/homematicip_cloud/.translations/cs.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "P\u0159\u00edstupov\u00fd bod je ji\u017e nakonfigurov\u00e1n", + "conection_aborted": "Nelze se p\u0159ipojit k serveru HMIP", + "unknown": "Do\u0161lo k nezn\u00e1m\u00e9 chyb\u011b" + }, + "error": { + "invalid_pin": "Neplatn\u00fd k\u00f3d PIN, zkuste to znovu.", + "press_the_button": "Stiskn\u011bte modr\u00e9 tla\u010d\u00edtko.", + "register_failed": "Registrace se nezda\u0159ila, zkuste to znovu.", + "timeout_button": "\u010casov\u00fd limit stisknut\u00ed modr\u00e9ho tla\u010d\u00edtka vypr\u0161el. Zkuste to znovu." + }, + "step": { + "init": { + "data": { + "hapid": "ID p\u0159\u00edstupov\u00e9ho bodu (SGTIN)", + "name": "N\u00e1zev (nepovinn\u00e9, pou\u017e\u00edv\u00e1 se jako p\u0159edpona n\u00e1zvu pro v\u0161echna za\u0159\u00edzen\u00ed)", + "pin": "Pin k\u00f3d (nepovinn\u00e9)" + }, + "title": "Vyberte p\u0159\u00edstupov\u00fd bod HomematicIP" + }, + "link": { + "description": "Stiskn\u011bte modr\u00e9 tla\u010d\u00edtko na p\u0159\u00edstupov\u00e9m bodu a tla\u010d\u00edtko pro registraci HomematicIP s dom\u00e1c\u00edm asistentem. \n\n ! [Um\u00edst\u011bn\u00ed tla\u010d\u00edtka na za\u0159\u00edzen\u00ed] (/static/images/config_flows/config_homematicip_cloud.png)", + "title": "P\u0159ipojit se k p\u0159\u00edstupov\u00e9mu bodu" + } + }, + "title": "HomematicIP Cloud" + } +} \ No newline at end of file diff --git a/homeassistant/components/homematicip_cloud/.translations/de.json b/homeassistant/components/homematicip_cloud/.translations/de.json new file mode 100644 index 00000000000000..8e4130a32511d4 --- /dev/null +++ b/homeassistant/components/homematicip_cloud/.translations/de.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "Der Accesspoint ist bereits konfiguriert", + "conection_aborted": "Keine Verbindung zum HMIP-Server m\u00f6glich", + "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." + }, + "step": { + "init": { + "data": { + "hapid": "Accesspoint ID (SGTIN)", + "name": "Name (optional, wird als Pr\u00e4fix f\u00fcr alle Ger\u00e4te verwendet)", + "pin": "PIN Code (optional)" + } + }, + "link": { + "title": "Verkn\u00fcpfe den Accesspoint" + } + }, + "title": "HomematicIP Cloud" + } +} \ No newline at end of file diff --git a/homeassistant/components/homematicip_cloud/.translations/en.json b/homeassistant/components/homematicip_cloud/.translations/en.json index 887a3a5780b0eb..0cf99cd297582f 100644 --- a/homeassistant/components/homematicip_cloud/.translations/en.json +++ b/homeassistant/components/homematicip_cloud/.translations/en.json @@ -1,30 +1,30 @@ { - "config": { - "title": "HomematicIP Cloud", - "step": { - "init": { - "title": "Pick HomematicIP Accesspoint", - "data": { - "hapid": "Accesspoint ID (SGTIN)", - "pin": "Pin Code (optional)", - "name": "Name (optional, used as name prefix for all devices)" - } - }, - "link": { - "title": "Link Accesspoint", - "description": "Press the blue button on the accesspoint and the submit button to register HomematicIP with Home Assistant.\n\n![Location of button on bridge](/static/images/config_flows/config_homematicip_cloud.png)" - } - }, - "error": { - "register_failed": "Failed to register, please try again.", - "invalid_pin": "Invalid PIN, please try again.", - "press_the_button": "Please press the blue button.", - "timeout_button": "Blue button press timeout, please try again." - }, - "abort": { - "unknown": "Unknown error occurred.", - "conection_aborted": "Could not connect to HMIP server", - "already_configured": "Accesspoint is already configured" + "config": { + "abort": { + "already_configured": "Accesspoint is already configured", + "conection_aborted": "Could not connect to HMIP server", + "unknown": "Unknown error occurred." + }, + "error": { + "invalid_pin": "Invalid PIN, please try again.", + "press_the_button": "Please press the blue button.", + "register_failed": "Failed to register, please try again.", + "timeout_button": "Blue button press timeout, please try again." + }, + "step": { + "init": { + "data": { + "hapid": "Accesspoint ID (SGTIN)", + "name": "Name (optional, used as name prefix for all devices)", + "pin": "Pin Code (optional)" + }, + "title": "Pick HomematicIP Accesspoint" + }, + "link": { + "description": "Press the blue button on the accesspoint and the submit button to register HomematicIP with Home Assistant.\n\n![Location of button on bridge](/static/images/config_flows/config_homematicip_cloud.png)", + "title": "Link Accesspoint" + } + }, + "title": "HomematicIP Cloud" } - } -} +} \ No newline at end of file diff --git a/homeassistant/components/homematicip_cloud/.translations/es-419.json b/homeassistant/components/homematicip_cloud/.translations/es-419.json new file mode 100644 index 00000000000000..9af472893807b9 --- /dev/null +++ b/homeassistant/components/homematicip_cloud/.translations/es-419.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Accesspoint ya est\u00e1 configurado", + "conection_aborted": "No se pudo conectar al servidor HMIP", + "unknown": "Se produjo un error desconocido." + }, + "error": { + "invalid_pin": "PIN no v\u00e1lido, por favor intente de nuevo.", + "press_the_button": "Por favor, presione el bot\u00f3n azul.", + "register_failed": "No se pudo registrar, por favor intente de nuevo." + }, + "step": { + "init": { + "data": { + "hapid": "ID de punto de acceso (SGTIN)", + "name": "Nombre (opcional, usado como prefijo de nombre para todos los dispositivos)", + "pin": "C\u00f3digo PIN (opcional)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homematicip_cloud/.translations/hu.json b/homeassistant/components/homematicip_cloud/.translations/hu.json new file mode 100644 index 00000000000000..f2f22e6a49d1e1 --- /dev/null +++ b/homeassistant/components/homematicip_cloud/.translations/hu.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "HomematicIP Felh\u0151" + } +} \ No newline at end of file diff --git a/homeassistant/components/homematicip_cloud/.translations/ja.json b/homeassistant/components/homematicip_cloud/.translations/ja.json new file mode 100644 index 00000000000000..105a74157897b8 --- /dev/null +++ b/homeassistant/components/homematicip_cloud/.translations/ja.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "already_configured": "\u30a2\u30af\u30bb\u30b9\u30dd\u30a4\u30f3\u30c8\u306f\u65e2\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "conection_aborted": "HMIP\u30b5\u30fc\u30d0\u30fc\u306b\u63a5\u7d9a\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f", + "unknown": "\u4e0d\u660e\u306a\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f\u3002" + }, + "error": { + "invalid_pin": "PIN\u304c\u7121\u52b9\u3067\u3059\u3001\u3082\u3046\u4e00\u5ea6\u304a\u8a66\u3057\u304f\u3060\u3055\u3044\u3002", + "press_the_button": "\u9752\u3044\u30dc\u30bf\u30f3\u3092\u62bc\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homematicip_cloud/.translations/ko.json b/homeassistant/components/homematicip_cloud/.translations/ko.json new file mode 100644 index 00000000000000..e135873067eb86 --- /dev/null +++ b/homeassistant/components/homematicip_cloud/.translations/ko.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "\uc561\uc138\uc2a4 \ud3ec\uc778\ud2b8\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "conection_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" + }, + "error": { + "invalid_pin": "PIN\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", + "press_the_button": "\ud30c\ub780\uc0c9 \ubc84\ud2bc\uc744 \ub20c\ub7ec\uc8fc\uc138\uc694.", + "register_failed": "\ub4f1\ub85d\uc5d0 \uc2e4\ud328\ud558\uc600\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", + "timeout_button": "\uc815\ud574\uc9c4 \uc2dc\uac04\ub0b4\uc5d0 \ud30c\ub780\uc0c9 \ubc84\ud2bc\uc744 \ub20c\ub974\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694." + }, + "step": { + "init": { + "data": { + "hapid": "\uc561\uc138\uc2a4 \ud3ec\uc778\ud2b8 ID (SGTIN)", + "name": "\uc774\ub984 (\uc120\ud0dd \uc0ac\ud56d, \ubaa8\ub4e0 \uc7a5\uce58 \uc774\ub984\uc758 \uc811\ub450\uc5b4\ub85c \uc0ac\uc6a9)", + "pin": "PIN \ucf54\ub4dc (\uc120\ud0dd\uc0ac\ud56d)" + }, + "title": "HomematicIP \uc561\uc138\uc2a4 \ud3ec\uc778\ud2b8 \uc120\ud0dd" + }, + "link": { + "description": "Home Assistant\uc5d0 HomematicIP\ub97c \ub4f1\ub85d\ud558\ub824\uba74 \uc561\uc138\uc2a4 \ud3ec\uc778\ud2b8\uc758 \ud30c\ub780\uc0c9 \ubc84\ud2bc\uacfc \uc11c\ubc0b \ubc84\ud2bc\uc744 \ub20c\ub7ec\uc8fc\uc138\uc694.\n\n![\ube0c\ub9bf\uc9c0\uc758 \ubc84\ud2bc \uc704\uce58 \ubcf4\uae30](/static/images/config_flows/config_homematicip_cloud.png)", + "title": "\uc561\uc138\uc2a4 \ud3ec\uc778\ud2b8\uc5d0 \uc5f0\uacb0" + } + }, + "title": "HomematicIP \ud074\ub77c\uc6b0\ub4dc" + } +} \ No newline at end of file diff --git a/homeassistant/components/homematicip_cloud/.translations/lb.json b/homeassistant/components/homematicip_cloud/.translations/lb.json new file mode 100644 index 00000000000000..3dd3f1a5dca388 --- /dev/null +++ b/homeassistant/components/homematicip_cloud/.translations/lb.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "Acesspoint ass schon konfigur\u00e9iert", + "conection_aborted": "Konnt sech net mam HMIP Server verbannen", + "unknown": "Onbekannten Feeler opgetrueden" + }, + "error": { + "invalid_pin": "Ong\u00ebltege Pin, prob\u00e9iert w.e.g. nach emol.", + "press_the_button": "Dr\u00e9ckt w.e.g. de bloe Kn\u00e4ppchen.", + "register_failed": "Feeler beim registr\u00e9ieren, prob\u00e9iert w.e.g. nach emol.", + "timeout_button": "Z\u00e4itiwwerschreidung beim dr\u00e9cken vum bloe Kn\u00e4ppchen, prob\u00e9iert w.e.g. nach emol." + }, + "step": { + "init": { + "data": { + "hapid": "ID vum Accesspoint (SGTIN)", + "name": "Numm (optional, g\u00ebtt als prefixe fir all Apparat benotzt)", + "pin": "Pin Code (Optional)" + }, + "title": "HomematicIP Accesspoint auswielen" + }, + "link": { + "description": "Dr\u00e9ckt de bloen Kn\u00e4ppchen um Accesspoint an den Submit Kn\u00e4ppchen fir d'HomematicIP mam Home Assistant ze registr\u00e9ieren.", + "title": "Accesspoint verbannen" + } + }, + "title": "HomematicIP Cloud" + } +} \ No newline at end of file diff --git a/homeassistant/components/homematicip_cloud/.translations/nl.json b/homeassistant/components/homematicip_cloud/.translations/nl.json new file mode 100644 index 00000000000000..0559dae4bfd66c --- /dev/null +++ b/homeassistant/components/homematicip_cloud/.translations/nl.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "Accesspoint is reeds geconfigureerd", + "conection_aborted": "Kon geen verbinding maken met de HMIP-server", + "unknown": "Er is een onbekende fout opgetreden." + }, + "error": { + "invalid_pin": "Ongeldige PIN-code, probeer het nogmaals.", + "press_the_button": "Druk op de blauwe knop.", + "register_failed": "Kan niet registreren, gelieve opnieuw te proberen.", + "timeout_button": "Blauwe knop druk op timeout, probeer het opnieuw." + }, + "step": { + "init": { + "data": { + "hapid": "Accesspoint ID (SGTIN)", + "name": "Naam (optioneel, gebruikt als naamprefix voor alle apparaten)", + "pin": "Pin-Code (optioneel)" + }, + "title": "Kies HomematicIP Accesspoint" + }, + "link": { + "description": "Druk op de blauwe knop op de accesspoint en de verzendknop om HomematicIP met de Home Assistant te registreren. \n\n![Locatie van knop op brug](/static/images/config_flows/\nconfig_homematicip_cloud.png)", + "title": "Link Accesspoint" + } + }, + "title": "HomematicIP Cloud" + } +} \ No newline at end of file diff --git a/homeassistant/components/homematicip_cloud/.translations/no.json b/homeassistant/components/homematicip_cloud/.translations/no.json new file mode 100644 index 00000000000000..7e164abd3bb272 --- /dev/null +++ b/homeassistant/components/homematicip_cloud/.translations/no.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "Tilgangspunktet er allerede konfigurert", + "conection_aborted": "Kunne ikke koble til HMIP serveren", + "unknown": "Ukjent feil oppstod." + }, + "error": { + "invalid_pin": "Ugyldig PIN kode, pr\u00f8v igjen.", + "press_the_button": "Vennligst trykk p\u00e5 den bl\u00e5 knappen.", + "register_failed": "Kunne ikke registrere, vennligst pr\u00f8v igjen.", + "timeout_button": "Bl\u00e5 knapp-trykk tok for lang tid, vennligst pr\u00f8v igjen." + }, + "step": { + "init": { + "data": { + "hapid": "Tilgangspunkt ID (SGTIN)", + "name": "Navn (valgfritt, brukes som prefiks for alle enheter)", + "pin": "PIN kode (valgfritt)" + }, + "title": "Velg HomematicIP tilgangspunkt" + }, + "link": { + "description": "Trykk p\u00e5 den bl\u00e5 knappen p\u00e5 tilgangspunktet og send knappen for \u00e5 registrere HomematicIP med Home Assistant. \n\n![Plassering av knapp p\u00e5 bridge](/static/images/config_flows/config_homematicip_cloud.png)", + "title": "Link Tilgangspunkt" + } + }, + "title": "HomematicIP Sky" + } +} \ No newline at end of file diff --git a/homeassistant/components/homematicip_cloud/.translations/pl.json b/homeassistant/components/homematicip_cloud/.translations/pl.json new file mode 100644 index 00000000000000..c2ec6be4bd465a --- /dev/null +++ b/homeassistant/components/homematicip_cloud/.translations/pl.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "Punkt dost\u0119pu jest ju\u017c skonfigurowany", + "conection_aborted": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z serwerem HMIP", + "unknown": "Wyst\u0105pi\u0142 nieznany b\u0142\u0105d" + }, + "error": { + "invalid_pin": "Nieprawid\u0142owy kod PIN, spr\u00f3buj ponownie.", + "press_the_button": "Prosz\u0119 nacisn\u0105\u0107 niebieski przycisk.", + "register_failed": "Nie uda\u0142o si\u0119 zarejestrowa\u0107, spr\u00f3buj ponownie.", + "timeout_button": "Oczekiwania na naci\u015bni\u0119cie niebieskiego przycisku zako\u0144czone, spr\u00f3buj ponownie." + }, + "step": { + "init": { + "data": { + "hapid": "ID punktu dost\u0119pu (SGTIN)", + "name": "Nazwa (opcjonalnie, u\u017cywana jako prefiks nazwy dla wszystkich urz\u0105dze\u0144)", + "pin": "Kod PIN (opcjonalnie)" + }, + "title": "Wybierz punkt dost\u0119pu HomematicIP" + }, + "link": { + "description": "Naci\u015bnij niebieski przycisk na punkcie dost\u0119pu i przycisk przesy\u0142ania, aby zarejestrowa\u0107 HomematicIP w Home Assistant. \n\n![Location of button on bridge](/static/images/config_flows/config_homematicip_cloud.png)", + "title": "Po\u0142\u0105cz z punktem dost\u0119pu" + } + }, + "title": "Chmura HomematicIP" + } +} \ No newline at end of file diff --git a/homeassistant/components/homematicip_cloud/.translations/pt-BR.json b/homeassistant/components/homematicip_cloud/.translations/pt-BR.json new file mode 100644 index 00000000000000..6e5af1c26cc97a --- /dev/null +++ b/homeassistant/components/homematicip_cloud/.translations/pt-BR.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "O Accesspoint j\u00e1 est\u00e1 configurado", + "conection_aborted": "N\u00e3o foi poss\u00edvel conectar ao servidor HMIP", + "unknown": "Ocorreu um erro desconhecido." + }, + "error": { + "invalid_pin": "PIN inv\u00e1lido, por favor tente novamente.", + "press_the_button": "Por favor, pressione o bot\u00e3o azul.", + "register_failed": "Falha ao registrar, por favor tente novamente.", + "timeout_button": "Tempo para pressionar o Bot\u00e3o Azul expirou, por favor tente novamente." + }, + "step": { + "init": { + "data": { + "hapid": "ID do AccessPoint (SGTIN)", + "name": "Nome (opcional, usado como prefixo de nome para todos os dispositivos)", + "pin": "C\u00f3digo PIN (opcional)" + }, + "title": "Escolha um HomematicIP Accesspoint" + }, + "link": { + "description": "Pressione o bot\u00e3o azul no ponto de acesso e o bot\u00e3o enviar para registrar o HomematicIP com o Home Assistant.\n\n![Location of button on bridge](/static/images/config_flows/config_homematicip_cloud.png)", + "title": "Accesspoint link" + } + }, + "title": "Nuvem do HomematicIP" + } +} \ No newline at end of file diff --git a/homeassistant/components/homematicip_cloud/.translations/pt.json b/homeassistant/components/homematicip_cloud/.translations/pt.json new file mode 100644 index 00000000000000..ef742e2ce5ed06 --- /dev/null +++ b/homeassistant/components/homematicip_cloud/.translations/pt.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "O ponto de acesso j\u00e1 se encontra configurado", + "conection_aborted": "N\u00e3o foi poss\u00edvel ligar ao servidor HMIP", + "unknown": "Ocorreu um erro desconhecido." + }, + "error": { + "invalid_pin": "PIN inv\u00e1lido, por favor, tente novamente.", + "press_the_button": "Por favor, pressione o bot\u00e3o azul.", + "register_failed": "Falha ao registrar, por favor, tente novamente.", + "timeout_button": "Tempo limite ultrapassado para carregar bot\u00e3o azul, por favor, tente de novo." + }, + "step": { + "init": { + "data": { + "hapid": "ID do ponto de acesso (SGTIN)", + "name": "Nome (opcional, usado como prefixo de nome para todos os dispositivos)", + "pin": "C\u00f3digo PIN (opcional)" + }, + "title": "Escolher ponto de acesso HomematicIP" + }, + "link": { + "description": "Pressione o bot\u00e3o azul no accesspoint e o bot\u00e3o enviar para registrar HomematicIP com Home Assistant.\n\n! [Localiza\u00e7\u00e3o do bot\u00e3o na ponte] (/ static/images/config_flows/config_homematicip_cloud.png)", + "title": "Associar ponto de acesso" + } + }, + "title": "Nuvem do HomematicIP" + } +} \ No newline at end of file diff --git a/homeassistant/components/homematicip_cloud/.translations/ru.json b/homeassistant/components/homematicip_cloud/.translations/ru.json new file mode 100644 index 00000000000000..77c6469f64c67c --- /dev/null +++ b/homeassistant/components/homematicip_cloud/.translations/ru.json @@ -0,0 +1,30 @@ +{ + "config": { + "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", + "conection_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" + }, + "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.", + "press_the_button": "\u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043d\u0430\u0436\u043c\u0438\u0442\u0435 \u0441\u0438\u043d\u044e\u044e \u043a\u043d\u043e\u043f\u043a\u0443.", + "register_failed": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c\u0441\u044f, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0441\u043d\u043e\u0432\u0430", + "timeout_button": "\u0412\u044b \u043d\u0435 \u043d\u0430\u0436\u0430\u043b\u0438 \u0441\u0438\u043d\u0438\u044e \u043a\u043d\u043e\u043f\u043a\u0443 \u0432 \u043f\u0440\u0435\u0434\u0435\u043b\u0430\u0445 \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u043e\u0433\u043e \u0432\u0440\u0435\u043c\u0435\u043d\u0438, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0441\u043d\u043e\u0432\u0430" + }, + "step": { + "init": { + "data": { + "hapid": "\u0418\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u0442\u043e\u0447\u043a\u0438 \u0434\u043e\u0441\u0442\u0443\u043f\u0430 (SGTIN)", + "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" + }, + "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)", + "title": "\u041f\u0440\u0438\u0432\u044f\u0437\u0430\u0442\u044c \u0442\u043e\u0447\u043a\u0443 \u0434\u043e\u0441\u0442\u0443\u043f\u0430" + } + }, + "title": "HomematicIP Cloud" + } +} \ No newline at end of file diff --git a/homeassistant/components/homematicip_cloud/.translations/sl.json b/homeassistant/components/homematicip_cloud/.translations/sl.json new file mode 100644 index 00000000000000..d9749480c0d59b --- /dev/null +++ b/homeassistant/components/homematicip_cloud/.translations/sl.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "Dostopna to\u010dka je \u017ee konfigurirana", + "conection_aborted": "Povezave s stre\u017enikom HMIP ni bila mogo\u010da", + "unknown": "Pri\u0161lo je do neznane napake" + }, + "error": { + "invalid_pin": "Neveljavna koda PIN, poskusite znova.", + "press_the_button": "Prosimo, pritisnite modri gumb.", + "register_failed": "Registracija ni uspela, poskusite znova", + "timeout_button": "Potekla je \u010dasovna omejitev za pritisk modrega gumba, poskusite znova." + }, + "step": { + "init": { + "data": { + "hapid": "ID dostopne to\u010dke (SGTIN)", + "name": "Ime (neobvezno, ki se uporablja kot predpona za vse naprave)", + "pin": "Koda PIN (neobvezno)" + }, + "title": "Izberite dostopno to\u010dko HomematicIP" + }, + "link": { + "description": "Pritisnite modro tipko na dostopni to\u010dko in gumb po\u0161lji, da registrirate homematicIP s Home Assistentom. \n\n![Location of button on bridge](/static/images/config_flows/config_homematicip_cloud.png)", + "title": "Pove\u017eite dostopno to\u010dno" + } + }, + "title": "HomematicIP Cloud" + } +} \ No newline at end of file diff --git a/homeassistant/components/homematicip_cloud/.translations/sv.json b/homeassistant/components/homematicip_cloud/.translations/sv.json new file mode 100644 index 00000000000000..945dca8a277c79 --- /dev/null +++ b/homeassistant/components/homematicip_cloud/.translations/sv.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "Accesspunkten \u00e4r redan konfigurerad", + "conection_aborted": "Kunde inte ansluta till HMIP server", + "unknown": "Ett ok\u00e4nt fel har intr\u00e4ffat" + }, + "error": { + "invalid_pin": "Ogiltig PIN-kod, f\u00f6rs\u00f6k igen.", + "press_the_button": "V\u00e4nligen tryck p\u00e5 den bl\u00e5 knappen.", + "register_failed": "Misslyckades med att registrera, f\u00f6rs\u00f6k igen.", + "timeout_button": "Bl\u00e5 knapptryckning timeout, f\u00f6rs\u00f6k igen." + }, + "step": { + "init": { + "data": { + "hapid": "Accesspunkt-ID (SGTIN)", + "name": "Namn (frivilligt, anv\u00e4nds som namnprefix f\u00f6r alla enheter)", + "pin": "Pin-kod (frivilligt)" + }, + "title": "V\u00e4lj HomematicIP Accesspunkt" + }, + "link": { + "description": "Tryck p\u00e5 den bl\u00e5 knappen p\u00e5 accesspunkten och p\u00e5 skickaknappen f\u00f6r att registrera HomematicIP med Home-Assistant. \n\n ![Placering av knapp p\u00e5 bryggan](/static/images/config_flows/config_homematicip_cloud.png)", + "title": "L\u00e4nka Accesspunkt" + } + }, + "title": "HomematicIP Moln" + } +} \ No newline at end of file diff --git a/homeassistant/components/homematicip_cloud/.translations/zh-Hans.json b/homeassistant/components/homematicip_cloud/.translations/zh-Hans.json new file mode 100644 index 00000000000000..38970e4a97cad5 --- /dev/null +++ b/homeassistant/components/homematicip_cloud/.translations/zh-Hans.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "\u63a5\u5165\u70b9\u5df2\u7ecf\u914d\u7f6e\u5b8c\u6210", + "conection_aborted": "\u65e0\u6cd5\u8fde\u63a5\u5230 HMIP \u670d\u52a1\u5668", + "unknown": "\u53d1\u751f\u672a\u77e5\u9519\u8bef\u3002" + }, + "error": { + "invalid_pin": "PIN \u65e0\u6548\uff0c\u8bf7\u518d\u8bd5\u4e00\u6b21\u3002", + "press_the_button": "\u8bf7\u6309\u4e0b\u84dd\u8272\u6309\u94ae\u3002", + "register_failed": "\u6ce8\u518c\u5931\u8d25\uff0c\u8bf7\u91cd\u8bd5", + "timeout_button": "\u6309\u4e0b\u84dd\u8272\u6309\u94ae\u8d85\u65f6\uff0c\u8bf7\u518d\u8bd5\u4e00\u6b21\u3002" + }, + "step": { + "init": { + "data": { + "hapid": "\u63a5\u5165\u70b9 ID (SGTIN)", + "name": "\u540d\u79f0\uff08\u53ef\u9009\uff0c\u7528\u4f5c\u6240\u6709\u8bbe\u5907\u7684\u540d\u79f0\u524d\u7f00\uff09", + "pin": "PIN \u7801\uff08\u53ef\u9009\uff09" + }, + "title": "\u9009\u62e9 HomematicIP \u63a5\u5165\u70b9" + }, + "link": { + "description": "\u6309\u4e0b\u63a5\u5165\u70b9\u4e0a\u7684\u84dd\u8272\u6309\u94ae\u7136\u540e\u70b9\u51fb\u63d0\u4ea4\u6309\u94ae\uff0c\u4ee5\u5c06 HomematicIP \u6ce8\u518c\u5230 Home Assistant\u3002\n\n![\u63a5\u5165\u70b9\u7684\u6309\u94ae\u4f4d\u7f6e]\n(/static/images/config_flows/config_homematicip_cloud.png)", + "title": "\u8fde\u63a5\u63a5\u5165\u70b9" + } + }, + "title": "HomematicIP Cloud" + } +} \ No newline at end of file diff --git a/homeassistant/components/homematicip_cloud/.translations/zh-Hant.json b/homeassistant/components/homematicip_cloud/.translations/zh-Hant.json new file mode 100644 index 00000000000000..d8c6cff9b0cc65 --- /dev/null +++ b/homeassistant/components/homematicip_cloud/.translations/zh-Hant.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "Accesspoint \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "conection_aborted": "\u7121\u6cd5\u9023\u7dda\u81f3 HMIP \u4f3a\u670d\u5668", + "unknown": "\u767c\u751f\u672a\u77e5\u932f\u8aa4\u3002" + }, + "error": { + "invalid_pin": "PIN \u78bc\u7121\u6548\uff0c\u8acb\u518d\u8a66\u4e00\u6b21\u3002", + "press_the_button": "\u8acb\u6309\u4e0b\u85cd\u8272\u6309\u9215\u3002", + "register_failed": "\u8a3b\u518a\u5931\u6557\uff0c\u8acb\u518d\u8a66\u4e00\u6b21\u3002", + "timeout_button": "\u85cd\u8272\u6309\u9215\u903e\u6642\uff0c\u8acb\u518d\u8a66\u4e00\u6b21\u3002" + }, + "step": { + "init": { + "data": { + "hapid": "Accesspoint ID (SGTIN)", + "name": "\u540d\u7a31\uff08\u9078\u9805\uff0c\u7528\u4ee5\u4f5c\u70ba\u6240\u6709\u88dd\u7f6e\u7684\u5b57\u9996\u7528\uff09", + "pin": "PIN \u78bc\uff08\u9078\u9805\uff09" + }, + "title": "\u9078\u64c7 HomematicIP Accesspoint" + }, + "link": { + "description": "\u6309\u4e0b AP \u4e0a\u7684\u85cd\u8272\u6309\u9215\u8207\u50b3\u9001\u6309\u9215\uff0c\u4ee5\u65bc Home Assistant \u4e0a\u9032\u884c HomematicIP \u8a3b\u518a\u3002\n\n![\u6a4b\u63a5\u5668\u4e0a\u7684\u6309\u9215\u4f4d\u7f6e](/static/images/config_flows/config_homematicip_cloud.png)", + "title": "\u9023\u7d50 Accesspoint" + } + }, + "title": "HomematicIP Cloud" + } +} \ No newline at end of file diff --git a/homeassistant/components/hue/.translations/es-419.json b/homeassistant/components/hue/.translations/es-419.json new file mode 100644 index 00000000000000..8efc9101d9a175 --- /dev/null +++ b/homeassistant/components/hue/.translations/es-419.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "all_configured": "Todos los puentes Philips Hue ya est\u00e1n configurados", + "already_configured": "El puente ya est\u00e1 configurado", + "cannot_connect": "No se puede conectar al puente", + "discover_timeout": "Incapaz de descubrir puentes Hue", + "no_bridges": "No se descubrieron puentes Philips Hue", + "unknown": "Se produjo un error desconocido" + }, + "error": { + "linking": "Se produjo un error de enlace desconocido.", + "register_failed": "No se pudo registrar, intente de nuevo" + }, + "step": { + "init": { + "data": { + "host": "Host" + } + } + }, + "title": "Philips Hue" + } +} \ No newline at end of file diff --git a/homeassistant/components/hue/.translations/ja.json b/homeassistant/components/hue/.translations/ja.json new file mode 100644 index 00000000000000..ccd260cb1cf2e0 --- /dev/null +++ b/homeassistant/components/hue/.translations/ja.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "unknown": "\u4e0d\u660e\u306a\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f" + }, + "step": { + "init": { + "data": { + "host": "\u30db\u30b9\u30c8" + } + } + }, + "title": "Philips Hue" + } +} \ No newline at end of file diff --git a/homeassistant/components/nest/.translations/de.json b/homeassistant/components/nest/.translations/de.json index 721eafa807fb6d..32c72ef7d96874 100644 --- a/homeassistant/components/nest/.translations/de.json +++ b/homeassistant/components/nest/.translations/de.json @@ -1,5 +1,15 @@ { "config": { + "abort": { + "already_setup": "Sie k\u00f6nnen nur ein einziges Nest-Konto konfigurieren.", + "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/)." + }, + "error": { + "internal_error": "Ein interner Fehler ist aufgetreten", + "invalid_code": "Ung\u00fcltiger Code", + "timeout": "Ein zeit\u00fcberschreitungs Fehler ist aufgetreten", + "unknown": "Ein unbekannter Fehler ist aufgetreten" + }, "step": { "init": { "data": { @@ -16,6 +26,6 @@ "title": "Nest-Konto verkn\u00fcpfen" } }, - "title": "" + "title": "Nest" } } \ No newline at end of file diff --git a/homeassistant/components/nest/.translations/es-419.json b/homeassistant/components/nest/.translations/es-419.json new file mode 100644 index 00000000000000..0dfb5283d8f31f --- /dev/null +++ b/homeassistant/components/nest/.translations/es-419.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "no_flows": "Debe configurar Nest antes de poder autenticarse con \u00e9l. [Lea las instrucciones] (https://www.home-assistant.io/components/nest/)." + }, + "error": { + "invalid_code": "Codigo invalido" + }, + "step": { + "init": { + "data": { + "flow_impl": "Proveedor" + }, + "description": "Seleccione a trav\u00e9s de qu\u00e9 proveedor de autenticaci\u00f3n desea autenticarse con Nest.", + "title": "Proveedor de autenticaci\u00f3n" + }, + "link": { + "data": { + "code": "C\u00f3digo PIN" + }, + "description": "Para vincular su cuenta Nest, [autorice su cuenta] ( {url} ). \n\n Despu\u00e9s de la autorizaci\u00f3n, copie y pegue el c\u00f3digo pin proporcionado a continuaci\u00f3n.", + "title": "Enlazar cuenta Nest" + } + }, + "title": "Nest" + } +} \ No newline at end of file diff --git a/homeassistant/components/nest/.translations/it.json b/homeassistant/components/nest/.translations/it.json index ca34179cf5b191..e4a19ebd521224 100644 --- a/homeassistant/components/nest/.translations/it.json +++ b/homeassistant/components/nest/.translations/it.json @@ -1,7 +1,23 @@ { "config": { + "abort": { + "already_setup": "\u00c8 possibile configurare un solo account Nest.", + "authorize_url_fail": "Errore sconoscioto nel generare l'url di autorizzazione", + "authorize_url_timeout": "Tempo scaduto nel generare l'url di autorizzazione", + "no_flows": "Devi configurare Nest prima di poter eseguire l'autenticazione. [Si prega di leggere le istruzioni] (https://www.home-assistant.io/components/nest/)." + }, + "error": { + "internal_error": "Errore interno nella convalida del codice", + "invalid_code": "Codice non valido", + "timeout": "Tempo scaduto per l'inserimento del codice di convalida", + "unknown": "Errore sconosciuto durante la convalida del codice" + }, "step": { "init": { + "data": { + "flow_impl": "Provider" + }, + "description": "Scegli tramite quale provider di autenticazione desideri autenticarti con Nest.", "title": "Fornitore di autenticazione" }, "link": { diff --git a/homeassistant/components/nest/.translations/ja.json b/homeassistant/components/nest/.translations/ja.json new file mode 100644 index 00000000000000..4335b7d16747d0 --- /dev/null +++ b/homeassistant/components/nest/.translations/ja.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Nest" + } +} \ No newline at end of file diff --git a/homeassistant/components/nest/.translations/pt-BR.json b/homeassistant/components/nest/.translations/pt-BR.json new file mode 100644 index 00000000000000..22b4f56fc97f08 --- /dev/null +++ b/homeassistant/components/nest/.translations/pt-BR.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_setup": "Voc\u00ea pode configurar somente uma conta do Nest", + "authorize_url_fail": "Erro desconhecido ao gerar um URL de autoriza\u00e7\u00e3o.", + "authorize_url_timeout": "Excedido tempo limite de url de autoriza\u00e7\u00e3o", + "no_flows": "Voc\u00ea precisa configurar o Nest antes de poder autenticar com ele. [Por favor leio as instru\u00e7\u00f5es](https://www.home-assistant.io/components/nest/)." + }, + "error": { + "internal_error": "Erro interno ao validar o c\u00f3digo", + "invalid_code": "C\u00f3digo inv\u00e1lido", + "timeout": "Excedido tempo limite para validar c\u00f3digo", + "unknown": "Erro desconhecido ao validar o c\u00f3digo" + }, + "step": { + "init": { + "data": { + "flow_impl": "Provedor" + }, + "description": "Escolha atrav\u00e9s de qual provedor de autentica\u00e7\u00e3o voc\u00ea deseja autenticar com o Nest.", + "title": "Provedor de Autentica\u00e7\u00e3o" + }, + "link": { + "data": { + "code": "C\u00f3digo PIN" + }, + "description": "Para vincular sua conta do Nest, [autorize sua conta] ( {url} ). \n\n Ap\u00f3s a autoriza\u00e7\u00e3o, copie e cole o c\u00f3digo PIN fornecido abaixo.", + "title": "Link da conta Nest" + } + }, + "title": "" + } +} \ No newline at end of file diff --git a/homeassistant/components/nest/.translations/pt.json b/homeassistant/components/nest/.translations/pt.json new file mode 100644 index 00000000000000..40743fe3ddbb6f --- /dev/null +++ b/homeassistant/components/nest/.translations/pt.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_setup": "S\u00f3 pode configurar uma \u00fanica conta Nest.", + "authorize_url_fail": "Erro desconhecido ao gerar um URL de autoriza\u00e7\u00e3o.", + "authorize_url_timeout": "Limite temporal ultrapassado ao gerar um URL de autoriza\u00e7\u00e3o.", + "no_flows": "\u00c9 necess\u00e1rio configurar o Nest antes de poder autenticar com ele. [Por favor, leia as instru\u00e7\u00f5es] (https://www.home-assistant.io/components/nest/)." + }, + "error": { + "internal_error": "Erro interno ao validar o c\u00f3digo", + "invalid_code": "C\u00f3digo inv\u00e1lido", + "timeout": "Limite temporal ultrapassado ao validar c\u00f3digo", + "unknown": "Erro desconhecido ao validar o c\u00f3digo" + }, + "step": { + "init": { + "data": { + "flow_impl": "Fornecedor" + }, + "description": "Escolha com qual fornecedor de autentica\u00e7\u00e3o deseja autenticar o Nest.", + "title": "Fornecedor de Autentica\u00e7\u00e3o" + }, + "link": { + "data": { + "code": "C\u00f3digo PIN" + }, + "description": "Para associar \u00e0 sua conta Nest, [autorizar sua conta]({url}).\n\nAp\u00f3s a autoriza\u00e7\u00e3o, copie e cole o c\u00f3digo pin fornecido abaixo.", + "title": "Associar conta Nest" + } + }, + "title": "" + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/season.es-419.json b/homeassistant/components/sensor/.translations/season.es-419.json new file mode 100644 index 00000000000000..65df6a58b10799 --- /dev/null +++ b/homeassistant/components/sensor/.translations/season.es-419.json @@ -0,0 +1,8 @@ +{ + "state": { + "autumn": "Oto\u00f1o", + "spring": "Primavera", + "summer": "Verano", + "winter": "Invierno" + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/season.lv.json b/homeassistant/components/sensor/.translations/season.lv.json new file mode 100644 index 00000000000000..a96e1112f71f31 --- /dev/null +++ b/homeassistant/components/sensor/.translations/season.lv.json @@ -0,0 +1,8 @@ +{ + "state": { + "autumn": "Rudens", + "spring": "Pavasaris", + "summer": "Vasara", + "winter": "Ziema" + } +} \ No newline at end of file diff --git a/homeassistant/components/sonos/.translations/de.json b/homeassistant/components/sonos/.translations/de.json index f1b76b0d155787..d0587036d245b2 100644 --- a/homeassistant/components/sonos/.translations/de.json +++ b/homeassistant/components/sonos/.translations/de.json @@ -1,14 +1,15 @@ { "config": { "abort": { - "no_devices_found": "Keine Sonos Ger\u00e4te im Netzwerk gefunden." + "no_devices_found": "Keine Sonos Ger\u00e4te im Netzwerk gefunden.", + "single_instance_allowed": "Nur eine einzige Konfiguration von Sonos ist notwendig." }, "step": { "confirm": { "description": "M\u00f6chten Sie Sonos konfigurieren?", - "title": "" + "title": "Sonos" } }, - "title": "" + "title": "Sonos" } } \ No newline at end of file diff --git a/homeassistant/components/sonos/.translations/es-419.json b/homeassistant/components/sonos/.translations/es-419.json new file mode 100644 index 00000000000000..ff6924389d6104 --- /dev/null +++ b/homeassistant/components/sonos/.translations/es-419.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "No se encontraron dispositivos Sonos en la red.", + "single_instance_allowed": "S\u00f3lo se necesita una \u00fanica configuraci\u00f3n de Sonos." + }, + "step": { + "confirm": { + "description": "\u00bfDesea configurar Sonos?", + "title": "Sonos" + } + }, + "title": "Sonos" + } +} \ No newline at end of file diff --git a/homeassistant/components/sonos/.translations/ko.json b/homeassistant/components/sonos/.translations/ko.json index 5453e4322cd094..89933f57425b9e 100644 --- a/homeassistant/components/sonos/.translations/ko.json +++ b/homeassistant/components/sonos/.translations/ko.json @@ -2,7 +2,7 @@ "config": { "abort": { "no_devices_found": "Sonos \uc7a5\uce58\uac00 \ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \ubc1c\uacac\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4.", - "single_instance_allowed": "Sonos\uc758 \ub2e8\uc77c \uad6c\uc131 \ub9cc \ud544\uc694\ud569\ub2c8\ub2e4." + "single_instance_allowed": "\ud558\ub098\uc758 Sonos \ub9cc \uad6c\uc131 \ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4." }, "step": { "confirm": { diff --git a/homeassistant/components/sonos/.translations/pt-BR.json b/homeassistant/components/sonos/.translations/pt-BR.json new file mode 100644 index 00000000000000..02d3e0c0fb9c9b --- /dev/null +++ b/homeassistant/components/sonos/.translations/pt-BR.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Nenhum dispositivo Sonos encontrado na rede.", + "single_instance_allowed": "Apenas uma \u00fanica configura\u00e7\u00e3o do Sonos \u00e9 necess\u00e1ria." + }, + "step": { + "confirm": { + "description": "Voc\u00ea quer configurar o Sonos?", + "title": "" + } + }, + "title": "" + } +} \ No newline at end of file diff --git a/homeassistant/components/sonos/.translations/pt.json b/homeassistant/components/sonos/.translations/pt.json new file mode 100644 index 00000000000000..a2032c76a4a449 --- /dev/null +++ b/homeassistant/components/sonos/.translations/pt.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Nenhum dispositivo Sonos encontrado na rede.", + "single_instance_allowed": "Apenas uma \u00fanica configura\u00e7\u00e3o do Sonos \u00e9 necess\u00e1ria." + }, + "step": { + "confirm": { + "description": "Deseja configurar o Sonos?", + "title": "" + } + }, + "title": "" + } +} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/es-419.json b/homeassistant/components/zone/.translations/es-419.json new file mode 100644 index 00000000000000..b15be44b7b1ebb --- /dev/null +++ b/homeassistant/components/zone/.translations/es-419.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "name_exists": "El nombre ya existe" + }, + "step": { + "init": { + "data": { + "icon": "Icono", + "latitude": "Latitud", + "longitude": "Longitud", + "name": "Nombre", + "passive": "Pasivo", + "radius": "Radio" + }, + "title": "Definir par\u00e1metros de zona" + } + }, + "title": "Zona" + } +} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/ja.json b/homeassistant/components/zone/.translations/ja.json new file mode 100644 index 00000000000000..093f5ad99385aa --- /dev/null +++ b/homeassistant/components/zone/.translations/ja.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "init": { + "data": { + "latitude": "\u7def\u5ea6", + "longitude": "\u7d4c\u5ea6", + "name": "\u540d\u524d" + } + } + } + } +} \ No newline at end of file From eb5f6efb433c263248ab4f6eeef6b7a927e0fe1d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 2 Aug 2018 14:23:40 +0200 Subject: [PATCH 024/117] Update frontend to 20180802.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 5bf2b193957d3b..bf17cf593825c8 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -26,7 +26,7 @@ from homeassistant.loader import bind_hass from homeassistant.util.yaml import load_yaml -REQUIREMENTS = ['home-assistant-frontend==20180726.0'] +REQUIREMENTS = ['home-assistant-frontend==20180802.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log', diff --git a/requirements_all.txt b/requirements_all.txt index aa619142e0174a..036c6e0f0fd09e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -421,7 +421,7 @@ hole==0.3.0 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180726.0 +home-assistant-frontend==20180802.0 # homeassistant.components.homekit_controller # homekit==0.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3d8402842f1a12..2ae6f5aa797ce9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.2 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180726.0 +home-assistant-frontend==20180802.0 # homeassistant.components.homematicip_cloud homematicip==0.9.8 From 48af5116b34f1d19c5be2f913ae06831e871034f Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Thu, 2 Aug 2018 19:13:48 +0100 Subject: [PATCH 025/117] Update pymediaroom to 0.6.4 (#15786) * Dependency version bump * bump version --- homeassistant/components/media_player/mediaroom.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/media_player/mediaroom.py b/homeassistant/components/media_player/mediaroom.py index f5b7567aa348c9..32f1bb6e0ae811 100644 --- a/homeassistant/components/media_player/mediaroom.py +++ b/homeassistant/components/media_player/mediaroom.py @@ -25,7 +25,7 @@ ) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['pymediaroom==0.6.3'] +REQUIREMENTS = ['pymediaroom==0.6.4'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 036c6e0f0fd09e..00d6df6b06e4a8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -920,7 +920,7 @@ pylutron==0.1.0 pymailgunner==1.4 # homeassistant.components.media_player.mediaroom -pymediaroom==0.6.3 +pymediaroom==0.6.4 # homeassistant.components.media_player.xiaomi_tv pymitv==1.4.0 From 38928c4c0e0e0f13f86aec14fa2b8e1d87fb73ea Mon Sep 17 00:00:00 2001 From: Bryan York Date: Thu, 2 Aug 2018 13:09:19 -0700 Subject: [PATCH 026/117] Fix Min/Max Kelvin color temp attribute for Google (#15697) * Fix Min/Max Kelvin color temp attribute for Google Max Kelvin is actually Min Mireds and vice-versa. K = 1000000 / mireds * Update test_smart_home.py * Update test_trait.py --- homeassistant/components/google_assistant/trait.py | 6 ++++-- tests/components/google_assistant/test_smart_home.py | 4 ++-- tests/components/google_assistant/test_trait.py | 4 ++-- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index 74ee55c5e933e0..1d369eb87dad9e 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -304,10 +304,12 @@ def supported(domain, features): def sync_attributes(self): """Return color temperature attributes for a sync request.""" attrs = self.state.attributes + # Max Kelvin is Min Mireds K = 1000000 / mireds + # Min Kevin is Max Mireds K = 1000000 / mireds return { - 'temperatureMinK': color_util.color_temperature_mired_to_kelvin( - attrs.get(light.ATTR_MIN_MIREDS)), 'temperatureMaxK': color_util.color_temperature_mired_to_kelvin( + attrs.get(light.ATTR_MIN_MIREDS)), + 'temperatureMinK': color_util.color_temperature_mired_to_kelvin( attrs.get(light.ATTR_MAX_MIREDS)), } diff --git a/tests/components/google_assistant/test_smart_home.py b/tests/components/google_assistant/test_smart_home.py index cdaf4200c974e3..66e7747e06a776 100644 --- a/tests/components/google_assistant/test_smart_home.py +++ b/tests/components/google_assistant/test_smart_home.py @@ -74,8 +74,8 @@ async def test_sync_message(hass): 'willReportState': False, 'attributes': { 'colorModel': 'rgb', - 'temperatureMinK': 6535, - 'temperatureMaxK': 2000, + 'temperatureMinK': 2000, + 'temperatureMaxK': 6535, }, 'roomHint': 'Living Room' }] diff --git a/tests/components/google_assistant/test_trait.py b/tests/components/google_assistant/test_trait.py index e6336e05246038..1f7ee011e61bbf 100644 --- a/tests/components/google_assistant/test_trait.py +++ b/tests/components/google_assistant/test_trait.py @@ -414,8 +414,8 @@ async def test_color_temperature_light(hass): })) assert trt.sync_attributes() == { - 'temperatureMinK': 5000, - 'temperatureMaxK': 2000, + 'temperatureMinK': 2000, + 'temperatureMaxK': 5000, } assert trt.query_attributes() == { From affd4e7df36d7c063ee1a583b1e8cdcbea02d1e0 Mon Sep 17 00:00:00 2001 From: Jesse Rizzo <32472573+jesserizzo@users.noreply.github.com> Date: Thu, 2 Aug 2018 16:14:43 -0500 Subject: [PATCH 027/117] Add Enphase Envoy component (#15081) * add enphase envoy component * Add Enphase Envoy component for energy monitoring * Fix formatting problems * Fix formatting errors * Fix formatting errors * Fix formatting errors * Change unit of measurement to W or Wh. Return sensor states as integers * Fix formatting errors * Fix formatting errors * Fix formatting errors * Move import json to update function * Fix formatting. Add file to .coveragerc * Add new component to requirements_all.txt * Move API call to third party library on PyPi * Refactor * Run gen_requirements_all.py * Minor refactor * Fix indentation * Fix indentation --- .coveragerc | 1 + .../components/sensor/enphase_envoy.py | 107 ++++++++++++++++++ requirements_all.txt | 3 + 3 files changed, 111 insertions(+) create mode 100644 homeassistant/components/sensor/enphase_envoy.py diff --git a/.coveragerc b/.coveragerc index 5dd2c66b56eca2..f42bd5cd3db6e7 100644 --- a/.coveragerc +++ b/.coveragerc @@ -636,6 +636,7 @@ omit = homeassistant/components/sensor/eddystone_temperature.py homeassistant/components/sensor/eliqonline.py homeassistant/components/sensor/emoncms.py + homeassistant/components/sensor/enphase_envoy.py homeassistant/components/sensor/envirophat.py homeassistant/components/sensor/etherscan.py homeassistant/components/sensor/fastdotcom.py diff --git a/homeassistant/components/sensor/enphase_envoy.py b/homeassistant/components/sensor/enphase_envoy.py new file mode 100644 index 00000000000000..3c132fcf7df536 --- /dev/null +++ b/homeassistant/components/sensor/enphase_envoy.py @@ -0,0 +1,107 @@ +""" +Support for Enphase Envoy solar energy monitor. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.enphase_envoy/ +""" +import logging + +import voluptuous as vol + +from homeassistant.helpers.entity import Entity +from homeassistant.components.sensor import PLATFORM_SCHEMA +import homeassistant.helpers.config_validation as cv +from homeassistant.const import (CONF_IP_ADDRESS, CONF_MONITORED_CONDITIONS) + + +REQUIREMENTS = ['envoy_reader==0.1'] +_LOGGER = logging.getLogger(__name__) + +SENSORS = { + "production": ("Envoy Current Energy Production", 'W'), + "daily_production": ("Envoy Today's Energy Production", "Wh"), + "7_days_production": ("Envoy Last Seven Days Energy Production", "Wh"), + "lifetime_production": ("Envoy Lifetime Energy Production", "Wh"), + "consumption": ("Envoy Current Energy Consumption", "W"), + "daily_consumption": ("Envoy Today's Energy Consumption", "Wh"), + "7_days_consumption": ("Envoy Last Seven Days Energy Consumption", "Wh"), + "lifetime_consumption": ("Envoy Lifetime Energy Consumption", "Wh") + } + + +ICON = 'mdi:flash' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_IP_ADDRESS): cv.string, + vol.Optional(CONF_MONITORED_CONDITIONS, default=list(SENSORS)): + vol.All(cv.ensure_list, [vol.In(list(SENSORS))])}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the Enphase Envoy sensor.""" + ip_address = config[CONF_IP_ADDRESS] + monitored_conditions = config[CONF_MONITORED_CONDITIONS] + + # Iterate through the list of sensors + for condition in monitored_conditions: + add_devices([Envoy(ip_address, condition, SENSORS[condition][0], + SENSORS[condition][1])], True) + + +class Envoy(Entity): + """Implementation of the Enphase Envoy sensors.""" + + def __init__(self, ip_address, sensor_type, name, unit): + """Initialize the sensor.""" + self._ip_address = ip_address + self._name = name + self._unit_of_measurement = unit + self._type = sensor_type + self._state = None + + @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 unit_of_measurement(self): + """Return the unit of measurement of this entity, if any.""" + return self._unit_of_measurement + + @property + def icon(self): + """Icon to use in the frontend, if any.""" + return ICON + + def update(self): + """Get the energy production data from the Enphase Envoy.""" + import envoy_reader + + if self._type == "production": + self._state = int(envoy_reader.production(self._ip_address)) + elif self._type == "daily_production": + self._state = int(envoy_reader.daily_production(self._ip_address)) + elif self._type == "7_days_production": + self._state = int(envoy_reader.seven_days_production( + self._ip_address)) + elif self._type == "lifetime_production": + self._state = int(envoy_reader.lifetime_production( + self._ip_address)) + + elif self._type == "consumption": + self._state = int(envoy_reader.consumption(self._ip_address)) + elif self._type == "daily_consumption": + self._state = int(envoy_reader.daily_consumption( + self._ip_address)) + elif self._type == "7_days_consumption": + self._state = int(envoy_reader.seven_days_consumption( + self._ip_address)) + elif self._type == "lifetime_consumption": + self._state = int(envoy_reader.lifetime_consumption( + self._ip_address)) diff --git a/requirements_all.txt b/requirements_all.txt index 00d6df6b06e4a8..3c67f00e5a068d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -308,6 +308,9 @@ enocean==0.40 # homeassistant.components.sensor.envirophat # envirophat==0.0.6 +# homeassistant.components.sensor.enphase_envoy +envoy_reader==0.1 + # homeassistant.components.sensor.season ephem==3.7.6.0 From 59f8a73676bd28a65f1f5ba3fd17980ca9c468cb Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 3 Aug 2018 00:36:37 +0200 Subject: [PATCH 028/117] Return True from Nest setup (#15797) --- homeassistant/components/nest/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/nest/__init__.py b/homeassistant/components/nest/__init__.py index 4406062c82140f..1adb113bb81f94 100644 --- a/homeassistant/components/nest/__init__.py +++ b/homeassistant/components/nest/__init__.py @@ -93,7 +93,7 @@ async def async_nest_update_event_broker(hass, nest): async def async_setup(hass, config): """Set up Nest components.""" if DOMAIN not in config: - return + return True conf = config[DOMAIN] From b63312ff2e897565037b04f68cc2c401af828fac Mon Sep 17 00:00:00 2001 From: Conrad Juhl Andersen Date: Fri, 3 Aug 2018 04:49:38 +0200 Subject: [PATCH 029/117] Vacuum component: start_pause to individual start and pause commands. (#15751) * Add start and pause to StateVacuumDevice, move start_pause to VacuumDevice * Updated demo vacuum and tests * Add a few more tests --- homeassistant/components/vacuum/__init__.py | 65 +++++++++++++++---- homeassistant/components/vacuum/demo.py | 27 +++++--- homeassistant/components/vacuum/services.yaml | 14 ++++ tests/components/vacuum/test_demo.py | 23 ++++++- 4 files changed, 104 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/vacuum/__init__.py b/homeassistant/components/vacuum/__init__.py index 9cd9fd1c729673..97d009626b8248 100644 --- a/homeassistant/components/vacuum/__init__.py +++ b/homeassistant/components/vacuum/__init__.py @@ -45,6 +45,8 @@ SERVICE_SEND_COMMAND = 'send_command' SERVICE_SET_FAN_SPEED = 'set_fan_speed' SERVICE_START_PAUSE = 'start_pause' +SERVICE_START = 'start' +SERVICE_PAUSE = 'pause' SERVICE_STOP = 'stop' VACUUM_SERVICE_SCHEMA = vol.Schema({ @@ -65,6 +67,8 @@ SERVICE_TURN_OFF: {'method': 'async_turn_off'}, SERVICE_TOGGLE: {'method': 'async_toggle'}, SERVICE_START_PAUSE: {'method': 'async_start_pause'}, + SERVICE_START: {'method': 'async_start'}, + SERVICE_PAUSE: {'method': 'async_pause'}, SERVICE_RETURN_TO_BASE: {'method': 'async_return_to_base'}, SERVICE_CLEAN_SPOT: {'method': 'async_clean_spot'}, SERVICE_LOCATE: {'method': 'async_locate'}, @@ -97,6 +101,7 @@ SUPPORT_CLEAN_SPOT = 1024 SUPPORT_MAP = 2048 SUPPORT_STATE = 4096 +SUPPORT_START = 8192 @bind_hass @@ -155,6 +160,20 @@ def start_pause(hass, entity_id=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.""" @@ -242,18 +261,6 @@ def fan_speed_list(self): """Get the list of available fan speed steps of the vacuum cleaner.""" raise NotImplementedError() - def start_pause(self, **kwargs): - """Start, pause or resume the cleaning task.""" - raise NotImplementedError() - - async def async_start_pause(self, **kwargs): - """Start, pause or resume the cleaning task. - - This method must be run in the event loop. - """ - await self.hass.async_add_executor_job( - partial(self.start_pause, **kwargs)) - def stop(self, **kwargs): """Stop the vacuum cleaner.""" raise NotImplementedError() @@ -384,6 +391,18 @@ async def async_turn_off(self, **kwargs): await self.hass.async_add_executor_job( partial(self.turn_off, **kwargs)) + def start_pause(self, **kwargs): + """Start, pause or resume the cleaning task.""" + raise NotImplementedError() + + async def async_start_pause(self, **kwargs): + """Start, pause or resume the cleaning task. + + This method must be run in the event loop. + """ + await self.hass.async_add_executor_job( + partial(self.start_pause, **kwargs)) + class StateVacuumDevice(_BaseVacuum): """Representation of a vacuum cleaner robot that supports states.""" @@ -415,3 +434,25 @@ def state_attributes(self): data[ATTR_FAN_SPEED_LIST] = self.fan_speed_list return data + + def start(self): + """Start or resume the cleaning task.""" + raise NotImplementedError() + + async def async_start(self): + """Start or resume the cleaning task. + + This method must be run in the event loop. + """ + await self.hass.async_add_executor_job(self.start) + + def pause(self): + """Pause the cleaning task.""" + raise NotImplementedError() + + async def async_pause(self): + """Pause the cleaning task. + + This method must be run in the event loop. + """ + await self.hass.async_add_executor_job(self.pause) diff --git a/homeassistant/components/vacuum/demo.py b/homeassistant/components/vacuum/demo.py index 737be5e857b6af..5d4c6856a4dde2 100644 --- a/homeassistant/components/vacuum/demo.py +++ b/homeassistant/components/vacuum/demo.py @@ -10,8 +10,8 @@ ATTR_CLEANED_AREA, SUPPORT_BATTERY, SUPPORT_CLEAN_SPOT, SUPPORT_FAN_SPEED, SUPPORT_LOCATE, SUPPORT_PAUSE, SUPPORT_RETURN_HOME, SUPPORT_SEND_COMMAND, SUPPORT_STATUS, SUPPORT_STOP, SUPPORT_TURN_OFF, - SUPPORT_TURN_ON, SUPPORT_STATE, STATE_CLEANING, STATE_DOCKED, - STATE_IDLE, STATE_PAUSED, STATE_RETURNING, VacuumDevice, + SUPPORT_TURN_ON, SUPPORT_STATE, SUPPORT_START, STATE_CLEANING, + STATE_DOCKED, STATE_IDLE, STATE_PAUSED, STATE_RETURNING, VacuumDevice, StateVacuumDevice) _LOGGER = logging.getLogger(__name__) @@ -32,7 +32,7 @@ SUPPORT_STATE_SERVICES = SUPPORT_STATE | SUPPORT_PAUSE | SUPPORT_STOP | \ SUPPORT_RETURN_HOME | SUPPORT_FAN_SPEED | \ - SUPPORT_BATTERY | SUPPORT_CLEAN_SPOT + SUPPORT_BATTERY | SUPPORT_CLEAN_SPOT | SUPPORT_START FAN_SPEEDS = ['min', 'medium', 'high', 'max'] DEMO_VACUUM_COMPLETE = '0_Ground_floor' @@ -274,18 +274,25 @@ def device_state_attributes(self): """Return device state attributes.""" return {ATTR_CLEANED_AREA: round(self._cleaned_area, 2)} - def start_pause(self, **kwargs): - """Start, pause or resume the cleaning task.""" - if self.supported_features & SUPPORT_PAUSE == 0: + def start(self): + """Start or resume the cleaning task.""" + if self.supported_features & SUPPORT_START == 0: return - if self._state == STATE_CLEANING: - self._state = STATE_PAUSED - else: + if self._state != STATE_CLEANING: self._state = STATE_CLEANING self._cleaned_area += 1.32 self._battery_level -= 1 - self.schedule_update_ha_state() + self.schedule_update_ha_state() + + def pause(self): + """Pause the cleaning task.""" + if self.supported_features & SUPPORT_PAUSE == 0: + return + + if self._state == STATE_CLEANING: + self._state = STATE_PAUSED + self.schedule_update_ha_state() def stop(self, **kwargs): """Stop the cleaning task, do not return to dock.""" diff --git a/homeassistant/components/vacuum/services.yaml b/homeassistant/components/vacuum/services.yaml index 863157074bce19..6e40b3d67fc8eb 100644 --- a/homeassistant/components/vacuum/services.yaml +++ b/homeassistant/components/vacuum/services.yaml @@ -35,6 +35,20 @@ start_pause: description: Name of the vacuum entity. example: 'vacuum.xiaomi_vacuum_cleaner' +start: + description: Start or resume the cleaning task. + fields: + entity_id: + description: Name of the vacuum entity. + example: 'vacuum.xiaomi_vacuum_cleaner' + +pause: + description: Pause the cleaning task. + fields: + entity_id: + description: Name of the vacuum entity. + example: 'vacuum.xiaomi_vacuum_cleaner' + return_to_base: description: Tell the vacuum cleaner to return to its dock. fields: diff --git a/tests/components/vacuum/test_demo.py b/tests/components/vacuum/test_demo.py index b6c96567f5075c..bd6f2ae543c01d 100644 --- a/tests/components/vacuum/test_demo.py +++ b/tests/components/vacuum/test_demo.py @@ -83,7 +83,7 @@ def test_supported_features(self): self.assertEqual(STATE_OFF, state.state) state = self.hass.states.get(ENTITY_VACUUM_STATE) - self.assertEqual(5244, state.attributes.get(ATTR_SUPPORTED_FEATURES)) + self.assertEqual(13436, state.attributes.get(ATTR_SUPPORTED_FEATURES)) self.assertEqual(STATE_DOCKED, state.state) self.assertEqual(100, state.attributes.get(ATTR_BATTERY_LEVEL)) self.assertEqual("medium", state.attributes.get(ATTR_FAN_SPEED)) @@ -158,12 +158,12 @@ def test_methods(self): self.assertIn("spot", state.attributes.get(ATTR_STATUS)) self.assertEqual(STATE_ON, state.state) - vacuum.start_pause(self.hass, ENTITY_VACUUM_STATE) + vacuum.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.start_pause(self.hass, ENTITY_VACUUM_STATE) + vacuum.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) @@ -247,6 +247,23 @@ def test_unsupported_methods(self): self.assertNotIn("spot", state.attributes.get(ATTR_STATUS)) self.assertEqual(STATE_OFF, state.state) + # VacuumDevice should not support start and pause methods. + self.hass.states.set(ENTITY_VACUUM_COMPLETE, STATE_ON) + self.hass.block_till_done() + self.assertTrue(vacuum.is_on(self.hass, ENTITY_VACUUM_COMPLETE)) + + vacuum.pause(self.hass, ENTITY_VACUUM_COMPLETE) + self.hass.block_till_done() + self.assertTrue(vacuum.is_on(self.hass, ENTITY_VACUUM_COMPLETE)) + + self.hass.states.set(ENTITY_VACUUM_COMPLETE, STATE_OFF) + self.hass.block_till_done() + self.assertFalse(vacuum.is_on(self.hass, ENTITY_VACUUM_COMPLETE)) + + vacuum.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) self.hass.block_till_done() From ee180c51cf7e014e64545978e7deec237274f5f2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 3 Aug 2018 13:48:32 +0200 Subject: [PATCH 030/117] Bump frontend to 20180803.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 bf17cf593825c8..1d2e363d57461e 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -26,7 +26,7 @@ from homeassistant.loader import bind_hass from homeassistant.util.yaml import load_yaml -REQUIREMENTS = ['home-assistant-frontend==20180802.0'] +REQUIREMENTS = ['home-assistant-frontend==20180803.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log', diff --git a/requirements_all.txt b/requirements_all.txt index 3c67f00e5a068d..ce3d44b5246ab4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -424,7 +424,7 @@ hole==0.3.0 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180802.0 +home-assistant-frontend==20180803.0 # homeassistant.components.homekit_controller # homekit==0.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2ae6f5aa797ce9..6ce86a8cfc798c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.2 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180802.0 +home-assistant-frontend==20180803.0 # homeassistant.components.homematicip_cloud homematicip==0.9.8 From 6f2000f5e2289b395294cf5e0f4ab1217fc048e2 Mon Sep 17 00:00:00 2001 From: Jason Hu Date: Fri, 3 Aug 2018 04:52:34 -0700 Subject: [PATCH 031/117] Make sure use_x_forward_for and trusted_proxies must config together (#15804) * Make sure use_x_forward_for and trusted_proxies must config together * Fix unit test --- homeassistant/components/http/__init__.py | 8 +++---- tests/components/http/test_init.py | 28 +++++++++++++++++++++++ tests/scripts/test_check_config.py | 4 +--- 3 files changed, 33 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index 42629f752ad145..9f1b5995839db2 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -66,8 +66,8 @@ vol.Optional(CONF_SSL_KEY): cv.isfile, vol.Optional(CONF_CORS_ORIGINS, default=[]): vol.All(cv.ensure_list, [cv.string]), - vol.Optional(CONF_USE_X_FORWARDED_FOR, default=False): cv.boolean, - vol.Optional(CONF_TRUSTED_PROXIES, default=[]): + vol.Inclusive(CONF_USE_X_FORWARDED_FOR, 'proxy'): cv.boolean, + vol.Inclusive(CONF_TRUSTED_PROXIES, 'proxy'): vol.All(cv.ensure_list, [ip_network]), vol.Optional(CONF_TRUSTED_NETWORKS, default=[]): vol.All(cv.ensure_list, [ip_network]), @@ -96,8 +96,8 @@ async def async_setup(hass, config): ssl_peer_certificate = conf.get(CONF_SSL_PEER_CERTIFICATE) ssl_key = conf.get(CONF_SSL_KEY) cors_origins = conf[CONF_CORS_ORIGINS] - use_x_forwarded_for = conf[CONF_USE_X_FORWARDED_FOR] - trusted_proxies = conf[CONF_TRUSTED_PROXIES] + use_x_forwarded_for = conf.get(CONF_USE_X_FORWARDED_FOR, False) + trusted_proxies = conf.get(CONF_TRUSTED_PROXIES, []) trusted_networks = conf[CONF_TRUSTED_NETWORKS] is_ban_enabled = conf[CONF_IP_BAN_ENABLED] login_threshold = conf[CONF_LOGIN_ATTEMPTS_THRESHOLD] diff --git a/tests/components/http/test_init.py b/tests/components/http/test_init.py index d5368032a376bb..2ffaf17bebcca1 100644 --- a/tests/components/http/test_init.py +++ b/tests/components/http/test_init.py @@ -96,3 +96,31 @@ async def test_not_log_password(hass, aiohttp_client, caplog): # Ensure we don't log API passwords assert '/api/' in logs assert 'some-pass' not in logs + + +async def test_proxy_config(hass): + """Test use_x_forwarded_for must config together with trusted_proxies.""" + assert await async_setup_component(hass, 'http', { + 'http': { + http.CONF_USE_X_FORWARDED_FOR: True, + http.CONF_TRUSTED_PROXIES: ['127.0.0.1'] + } + }) is True + + +async def test_proxy_config_only_use_xff(hass): + """Test use_x_forwarded_for must config together with trusted_proxies.""" + assert await async_setup_component(hass, 'http', { + 'http': { + http.CONF_USE_X_FORWARDED_FOR: True + } + }) is not True + + +async def test_proxy_config_only_trust_proxies(hass): + """Test use_x_forwarded_for must config together with trusted_proxies.""" + assert await async_setup_component(hass, 'http', { + 'http': { + http.CONF_TRUSTED_PROXIES: ['127.0.0.1'] + } + }) is not True diff --git a/tests/scripts/test_check_config.py b/tests/scripts/test_check_config.py index 540f8d91da914e..59d8e27a672ee7 100644 --- a/tests/scripts/test_check_config.py +++ b/tests/scripts/test_check_config.py @@ -159,9 +159,7 @@ def test_secrets(self, isfile_patch): 'login_attempts_threshold': -1, 'server_host': '0.0.0.0', 'server_port': 8123, - 'trusted_networks': [], - 'trusted_proxies': [], - 'use_x_forwarded_for': False} + 'trusted_networks': []} assert res['secret_cache'] == {secrets_path: {'http_pw': 'abc123'}} assert res['secrets'] == {'http_pw': 'abc123'} assert normalize_yaml_files(res) == [ From 91e8680fc593e086a64645f6c09f857a616d85ed Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 3 Aug 2018 13:56:54 +0200 Subject: [PATCH 032/117] Only report color temp when in the correct color mode (#15791) --- homeassistant/components/light/deconz.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/light/deconz.py b/homeassistant/components/light/deconz.py index 4e1f5d8f15f9d5..20160edf8066f3 100644 --- a/homeassistant/components/light/deconz.py +++ b/homeassistant/components/light/deconz.py @@ -99,6 +99,9 @@ def effect_list(self): @property def color_temp(self): """Return the CT color value.""" + if self._light.colormode != 'ct': + return None + return self._light.ct @property From f6935b5d279a1f5a3c46581eca3a3392195032ff Mon Sep 17 00:00:00 2001 From: Jason Hu Date: Fri, 3 Aug 2018 05:23:26 -0700 Subject: [PATCH 033/117] Upgrade voluptuous-serialize to 2.0.0 (#15763) * Upgrade voluptuous-serialize to 2.0.0 * Change to 2.0.0 --- homeassistant/components/config/config_entries.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/config/config_entries.py b/homeassistant/components/config/config_entries.py index d2aa918eda2692..648f6ae9972c51 100644 --- a/homeassistant/components/config/config_entries.py +++ b/homeassistant/components/config/config_entries.py @@ -7,7 +7,7 @@ FlowManagerIndexView, FlowManagerResourceView) -REQUIREMENTS = ['voluptuous-serialize==1'] +REQUIREMENTS = ['voluptuous-serialize==2.0.0'] @asyncio.coroutine diff --git a/requirements_all.txt b/requirements_all.txt index ce3d44b5246ab4..0a7bd4f0961706 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1402,7 +1402,7 @@ uvcclient==0.10.1 venstarcolortouch==0.6 # homeassistant.components.config.config_entries -voluptuous-serialize==1 +voluptuous-serialize==2.0.0 # homeassistant.components.volvooncall volvooncall==0.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6ce86a8cfc798c..0a6c67112834ce 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -206,7 +206,7 @@ statsd==3.2.1 uvcclient==0.10.1 # homeassistant.components.config.config_entries -voluptuous-serialize==1 +voluptuous-serialize==2.0.0 # homeassistant.components.vultr vultr==0.1.2 From 0c7d46927e7bc66719f1436ab17c3d3166fc7132 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 4 Aug 2018 15:21:11 +0200 Subject: [PATCH 034/117] Bump frontend to 20180804.0 --- homeassistant/components/frontend/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 1d2e363d57461e..540341c68f2884 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -26,7 +26,7 @@ from homeassistant.loader import bind_hass from homeassistant.util.yaml import load_yaml -REQUIREMENTS = ['home-assistant-frontend==20180803.0'] +REQUIREMENTS = ['home-assistant-frontend==20180804.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log', From 3246b49a4529ea90c9d984623fc0f8db30a04dcf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sat, 4 Aug 2018 16:22:22 +0300 Subject: [PATCH 035/117] Upgrade pylint to 2.1.0 (#15811) * Upgrade pylint to 2.1.0 * Remove no longer needed pylint disables --- homeassistant/components/binary_sensor/bayesian.py | 1 - homeassistant/components/binary_sensor/threshold.py | 1 - homeassistant/components/calendar/todoist.py | 3 --- homeassistant/components/light/group.py | 2 -- homeassistant/components/sensor/min_max.py | 1 - homeassistant/components/sensor/statistics.py | 1 - homeassistant/helpers/template.py | 1 - homeassistant/scripts/benchmark/__init__.py | 2 -- pylintrc | 4 +--- requirements_test.txt | 2 +- requirements_test_all.txt | 2 +- 11 files changed, 3 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/binary_sensor/bayesian.py b/homeassistant/components/binary_sensor/bayesian.py index 72110eb50c9b44..75906e8ac5d58c 100644 --- a/homeassistant/components/binary_sensor/bayesian.py +++ b/homeassistant/components/binary_sensor/bayesian.py @@ -122,7 +122,6 @@ def __init__(self, name, prior, observations, probability_threshold, def async_added_to_hass(self): """Call when entity about to be added.""" @callback - # pylint: disable=invalid-name def async_threshold_sensor_state_listener(entity, old_state, new_state): """Handle sensor state changes.""" diff --git a/homeassistant/components/binary_sensor/threshold.py b/homeassistant/components/binary_sensor/threshold.py index 360671d1cea364..39681c894b3cc4 100644 --- a/homeassistant/components/binary_sensor/threshold.py +++ b/homeassistant/components/binary_sensor/threshold.py @@ -86,7 +86,6 @@ def __init__(self, hass, entity_id, name, lower, upper, hysteresis, self._state = False self.sensor_value = None - # pylint: disable=invalid-name @callback def async_threshold_sensor_state_listener( entity, old_state, new_state): diff --git a/homeassistant/components/calendar/todoist.py b/homeassistant/components/calendar/todoist.py index 30c5a6177b4aef..ba1f60027ba3cb 100644 --- a/homeassistant/components/calendar/todoist.py +++ b/homeassistant/components/calendar/todoist.py @@ -26,9 +26,6 @@ CONF_PROJECT_LABEL_WHITELIST = 'labels' CONF_PROJECT_WHITELIST = 'include_projects' -# https://github.com/PyCQA/pylint/pull/2320 -# pylint: disable=fixme - # Calendar Platform: Does this calendar event last all day? ALL_DAY = 'all_day' # Attribute: All tasks in this project diff --git a/homeassistant/components/light/group.py b/homeassistant/components/light/group.py index f9ffbb4e0bf72f..b2fdd36abe7edc 100644 --- a/homeassistant/components/light/group.py +++ b/homeassistant/components/light/group.py @@ -254,8 +254,6 @@ def _mean_tuple(*args): return tuple(sum(l) / len(l) for l in zip(*args)) -# https://github.com/PyCQA/pylint/issues/1831 -# pylint: disable=bad-whitespace def _reduce_attribute(states: List[State], key: str, default: Optional[Any] = None, diff --git a/homeassistant/components/sensor/min_max.py b/homeassistant/components/sensor/min_max.py index 912bf7b750010b..f3a30724732726 100644 --- a/homeassistant/components/sensor/min_max.py +++ b/homeassistant/components/sensor/min_max.py @@ -124,7 +124,6 @@ def __init__(self, hass, entity_ids, name, sensor_type, round_digits): self.states = {} @callback - # pylint: disable=invalid-name def async_min_max_sensor_state_listener(entity, old_state, new_state): """Handle the sensor state changes.""" if new_state.state is None or new_state.state in STATE_UNKNOWN: diff --git a/homeassistant/components/sensor/statistics.py b/homeassistant/components/sensor/statistics.py index a77509c18d4eb2..353330909105c5 100644 --- a/homeassistant/components/sensor/statistics.py +++ b/homeassistant/components/sensor/statistics.py @@ -97,7 +97,6 @@ def __init__(self, hass, entity_id, name, sampling_size, max_age): hass.async_add_job(self._initialize_from_database) @callback - # pylint: disable=invalid-name def async_stats_sensor_state_listener(entity, old_state, new_state): """Handle the sensor state changes.""" self._unit_of_measurement = new_state.attributes.get( diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index ea620c9bccd137..d0d3fb457b18e8 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -142,7 +142,6 @@ def render_with_possible_json_value(self, value, error_value=_SENTINEL): self.hass.loop, self.async_render_with_possible_json_value, value, error_value).result() - # pylint: disable=invalid-name def async_render_with_possible_json_value(self, value, error_value=_SENTINEL): """Render template with value exposed. diff --git a/homeassistant/scripts/benchmark/__init__.py b/homeassistant/scripts/benchmark/__init__.py index 331b99926274bf..98de59f2da15cd 100644 --- a/homeassistant/scripts/benchmark/__init__.py +++ b/homeassistant/scripts/benchmark/__init__.py @@ -78,7 +78,6 @@ def listener(_): @benchmark -# pylint: disable=invalid-name async def async_million_time_changed_helper(hass): """Run a million events through time changed helper.""" count = 0 @@ -109,7 +108,6 @@ def listener(_): @benchmark -# pylint: disable=invalid-name async def async_million_state_changed_helper(hass): """Run a million events through state changed helper.""" count = 0 diff --git a/pylintrc b/pylintrc index 00bc6582f3a038..b72502248d7b29 100644 --- a/pylintrc +++ b/pylintrc @@ -11,7 +11,6 @@ # too-few-* - same as too-many-* # abstract-method - with intro of async there are always methods missing # inconsistent-return-statements - doesn't handle raise -# useless-return - https://github.com/PyCQA/pylint/issues/2300 # not-an-iterable - https://github.com/PyCQA/pylint/issues/2311 disable= abstract-class-little-used, @@ -33,8 +32,7 @@ disable= too-many-public-methods, too-many-return-statements, too-many-statements, - unused-argument, - useless-return + unused-argument [REPORTS] reports=no diff --git a/requirements_test.txt b/requirements_test.txt index 225958a722cd07..366545831dce1c 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -8,7 +8,7 @@ flake8==3.5 mock-open==1.3.1 mypy==0.620 pydocstyle==1.1.1 -pylint==2.0.1 +pylint==2.1.0 pytest-aiohttp==0.3.0 pytest-cov==2.5.1 pytest-sugar==0.9.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0a6c67112834ce..953d974f298014 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -9,7 +9,7 @@ flake8==3.5 mock-open==1.3.1 mypy==0.620 pydocstyle==1.1.1 -pylint==2.0.1 +pylint==2.1.0 pytest-aiohttp==0.3.0 pytest-cov==2.5.1 pytest-sugar==0.9.1 From dbe44c076ede2d588fc0354327084f4d7301d1b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sat, 4 Aug 2018 16:22:37 +0300 Subject: [PATCH 036/117] Upgrade pytest to 3.7.1 and pytest-timeout to 1.3.1 (#15809) --- requirements_test.txt | 4 ++-- requirements_test_all.txt | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/requirements_test.txt b/requirements_test.txt index 366545831dce1c..12c5abff404cda 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -12,6 +12,6 @@ pylint==2.1.0 pytest-aiohttp==0.3.0 pytest-cov==2.5.1 pytest-sugar==0.9.1 -pytest-timeout==1.3.0 -pytest==3.6.3 +pytest-timeout==1.3.1 +pytest==3.7.1 requests_mock==1.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 953d974f298014..0760fd49770984 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -13,8 +13,8 @@ pylint==2.1.0 pytest-aiohttp==0.3.0 pytest-cov==2.5.1 pytest-sugar==0.9.1 -pytest-timeout==1.3.0 -pytest==3.6.3 +pytest-timeout==1.3.1 +pytest==3.7.1 requests_mock==1.5 From c7a8f1143c43567ccd3218c14d5a2ae6993bc927 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Sat, 4 Aug 2018 15:23:57 +0200 Subject: [PATCH 037/117] Fix rfxtrx device id matching (#15819) * Issue #15773 Fix PT2262 devices are incorrectly matched in rfxtrx component * style --- homeassistant/components/rfxtrx.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/rfxtrx.py b/homeassistant/components/rfxtrx.py index afe777ff7ccfdf..60dbb209039f8a 100644 --- a/homeassistant/components/rfxtrx.py +++ b/homeassistant/components/rfxtrx.py @@ -166,6 +166,7 @@ def get_pt2262_device(device_id): """Look for the device which id matches the given device_id parameter.""" for device in RFX_DEVICES.values(): if (hasattr(device, 'is_lighting4') and + device.masked_id is not None and device.masked_id == get_pt2262_deviceid(device_id, device.data_bits)): _LOGGER.debug("rfxtrx: found matching device %s for %s", From bfb9f2a00b3b7f889e5941e624de3efa227eff66 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Sat, 4 Aug 2018 22:24:17 +0200 Subject: [PATCH 038/117] Fix lint with wrong frontend version inside requirements_all --- requirements_all.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_all.txt b/requirements_all.txt index 0a7bd4f0961706..61e6295e66b2dd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -424,7 +424,7 @@ hole==0.3.0 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180803.0 +home-assistant-frontend==20180804.0 # homeassistant.components.homekit_controller # homekit==0.10 From 018bd8544cf128f5b0be2eabea230481909269da Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Sat, 4 Aug 2018 22:26:13 +0200 Subject: [PATCH 039/117] Fix lint with wrong frontend version inside requirements_test_all --- requirements_test_all.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0760fd49770984..83787946320b2b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.2 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180803.0 +home-assistant-frontend==20180804.0 # homeassistant.components.homematicip_cloud homematicip==0.9.8 From bce47eb9a4b938f899d19aef3bbb9eae75aff8fb Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Sat, 4 Aug 2018 22:35:41 +0200 Subject: [PATCH 040/117] Fix frontend requirements after bump (#15829) From 9ea3be4dc112fb45544fa089cb367c7f4f91b5f0 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 5 Aug 2018 02:46:14 +0200 Subject: [PATCH 041/117] Upgrade voluptuous to 0.11.5 (#15830) --- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index e832314cf1753e..29e10838f2196f 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -8,7 +8,7 @@ pip>=8.0.3 pytz>=2018.04 pyyaml>=3.13,<4 requests==2.19.1 -voluptuous==0.11.3 +voluptuous==0.11.5 # Breaks Python 3.6 and is not needed for our supported Python versions enum34==1000000000.0.0 diff --git a/requirements_all.txt b/requirements_all.txt index 61e6295e66b2dd..16beb1ef4a3af9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -9,7 +9,7 @@ pip>=8.0.3 pytz>=2018.04 pyyaml>=3.13,<4 requests==2.19.1 -voluptuous==0.11.3 +voluptuous==0.11.5 # homeassistant.components.nuimo_controller --only-binary=all nuimo==0.1.0 diff --git a/setup.py b/setup.py index 7519fc6a8736bc..b319df9067d41b 100755 --- a/setup.py +++ b/setup.py @@ -42,7 +42,7 @@ 'pytz>=2018.04', 'pyyaml>=3.13,<4', 'requests==2.19.1', - 'voluptuous==0.11.3', + 'voluptuous==0.11.5', ] MIN_PY_VERSION = '.'.join(map(str, hass_const.REQUIRED_PYTHON_VER)) From 5e1836f3a23fcbaa48e7db33a85e63690460e0fe Mon Sep 17 00:00:00 2001 From: fucm <39280548+fucm@users.noreply.github.com> Date: Sun, 5 Aug 2018 10:44:57 +0200 Subject: [PATCH 042/117] Add support for 2 Tahoma IO awning covers (#15660) * Add Tahoma io:VerticalExteriorAwningIOComponent and io:HorizontalAwningIOComponent * Fix position of horizontal awning cover * Add timestamps for lock time * Adjust open-close actions for horizontal awning cover * Fix stop action for io:RollerShutterGenericIOComponent * Remove redundant information * Use get for dict lookup --- homeassistant/components/cover/tahoma.py | 165 ++++++++++++++++++++--- homeassistant/components/tahoma.py | 2 + 2 files changed, 148 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/cover/tahoma.py b/homeassistant/components/cover/tahoma.py index 824e330d6a0712..b38a863ebe04d0 100644 --- a/homeassistant/components/cover/tahoma.py +++ b/homeassistant/components/cover/tahoma.py @@ -4,8 +4,10 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/cover.tahoma/ """ +from datetime import timedelta import logging +from homeassistant.util.dt import utcnow from homeassistant.components.cover import CoverDevice, ATTR_POSITION from homeassistant.components.tahoma import ( DOMAIN as TAHOMA_DOMAIN, TahomaDevice) @@ -14,6 +16,13 @@ _LOGGER = logging.getLogger(__name__) +ATTR_MEM_POS = 'memorized_position' +ATTR_RSSI_LEVEL = 'rssi_level' +ATTR_LOCK_START_TS = 'lock_start_ts' +ATTR_LOCK_END_TS = 'lock_end_ts' +ATTR_LOCK_LEVEL = 'lock_level' +ATTR_LOCK_ORIG = 'lock_originator' + def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Tahoma covers.""" @@ -27,27 +36,107 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class TahomaCover(TahomaDevice, CoverDevice): """Representation a Tahoma Cover.""" + def __init__(self, tahoma_device, controller): + """Initialize the device.""" + super().__init__(tahoma_device, controller) + + self._closure = 0 + # 100 equals open + self._position = 100 + self._closed = False + self._rssi_level = None + self._icon = None + # Can be 0 and bigger + self._lock_timer = 0 + self._lock_start_ts = None + self._lock_end_ts = None + # Can be 'comfortLevel1', 'comfortLevel2', 'comfortLevel3', + # 'comfortLevel4', 'environmentProtection', 'humanProtection', + # 'userLevel1', 'userLevel2' + self._lock_level = None + # Can be 'LSC', 'SAAC', 'SFC', 'UPS', 'externalGateway', 'localUser', + # 'myself', 'rain', 'security', 'temperature', 'timer', 'user', 'wind' + self._lock_originator = None + def update(self): """Update method.""" self.controller.get_states([self.tahoma_device]) + # For vertical covers + self._closure = self.tahoma_device.active_states.get( + 'core:ClosureState') + # For horizontal covers + if self._closure is None: + self._closure = self.tahoma_device.active_states.get( + 'core:DeploymentState') + + # For all, if available + if 'core:PriorityLockTimerState' in self.tahoma_device.active_states: + old_lock_timer = self._lock_timer + self._lock_timer = \ + self.tahoma_device.active_states['core:PriorityLockTimerState'] + # Derive timestamps from _lock_timer, only if not already set or + # something has changed + if self._lock_timer > 0: + _LOGGER.debug("Update %s, lock_timer: %d", self._name, + self._lock_timer) + if self._lock_start_ts is None: + self._lock_start_ts = utcnow() + if self._lock_end_ts is None or \ + old_lock_timer != self._lock_timer: + self._lock_end_ts = utcnow() +\ + timedelta(seconds=self._lock_timer) + else: + self._lock_start_ts = None + self._lock_end_ts = None + else: + self._lock_timer = 0 + self._lock_start_ts = None + self._lock_end_ts = None + + self._lock_level = self.tahoma_device.active_states.get( + 'io:PriorityLockLevelState') + + self._lock_originator = self.tahoma_device.active_states.get( + 'io:PriorityLockOriginatorState') + + self._rssi_level = self.tahoma_device.active_states.get( + 'core:RSSILevelState') + + # Define which icon to use + if self._lock_timer > 0: + if self._lock_originator == 'wind': + self._icon = 'mdi:weather-windy' + else: + self._icon = 'mdi:lock-alert' + else: + self._icon = None + + # Define current position. + # _position: 0 is closed, 100 is fully open. + # 'core:ClosureState': 100 is closed, 0 is fully open. + if self._closure is not None: + self._position = 100 - self._closure + if self._position <= 5: + self._position = 0 + if self._position >= 95: + self._position = 100 + self._closed = self._position == 0 + else: + self._position = None + if 'core:OpenClosedState' in self.tahoma_device.active_states: + self._closed = \ + self.tahoma_device.active_states['core:OpenClosedState']\ + == 'closed' + else: + self._closed = False + + _LOGGER.debug("Update %s, position: %d", self._name, self._position) + @property def current_cover_position(self): - """ - Return current position of cover. - - 0 is closed, 100 is fully open. - """ - try: - position = 100 - \ - self.tahoma_device.active_states['core:ClosureState'] - if position <= 5: - return 0 - if position >= 95: - return 100 - return position - except KeyError: - return None + """Return current position of cover.""" + return self._position def set_cover_position(self, **kwargs): """Move the cover to a specific position.""" @@ -56,8 +145,7 @@ def set_cover_position(self, **kwargs): @property def is_closed(self): """Return if the cover is closed.""" - if self.current_cover_position is not None: - return self.current_cover_position == 0 + return self._closed @property def device_class(self): @@ -66,13 +154,47 @@ def device_class(self): return 'window' return None + @property + def device_state_attributes(self): + """Return the device state attributes.""" + attr = {} + super_attr = super().device_state_attributes + if super_attr is not None: + attr.update(super_attr) + + if 'core:Memorized1PositionState' in self.tahoma_device.active_states: + attr[ATTR_MEM_POS] = self.tahoma_device.active_states[ + 'core:Memorized1PositionState'] + if self._rssi_level is not None: + attr[ATTR_RSSI_LEVEL] = self._rssi_level + if self._lock_start_ts is not None: + attr[ATTR_LOCK_START_TS] = self._lock_start_ts.isoformat() + if self._lock_end_ts is not None: + attr[ATTR_LOCK_END_TS] = self._lock_end_ts.isoformat() + if self._lock_level is not None: + attr[ATTR_LOCK_LEVEL] = self._lock_level + if self._lock_originator is not None: + attr[ATTR_LOCK_ORIG] = self._lock_originator + return attr + + @property + def icon(self): + """Return the icon to use in the frontend, if any.""" + return self._icon + def open_cover(self, **kwargs): """Open the cover.""" - self.apply_action('open') + if self.tahoma_device.type == 'io:HorizontalAwningIOComponent': + self.apply_action('close') + else: + self.apply_action('open') def close_cover(self, **kwargs): """Close the cover.""" - self.apply_action('close') + if self.tahoma_device.type == 'io:HorizontalAwningIOComponent': + self.apply_action('open') + else: + self.apply_action('close') def stop_cover(self, **kwargs): """Stop the cover.""" @@ -87,5 +209,10 @@ def stop_cover(self, **kwargs): 'rts:ExteriorVenetianBlindRTSComponent', 'rts:BlindRTSComponent'): self.apply_action('my') + elif self.tahoma_device.type in \ + ('io:HorizontalAwningIOComponent', + 'io:RollerShutterGenericIOComponent', + 'io:VerticalExteriorAwningIOComponent'): + self.apply_action('stop') else: self.apply_action('stopIdentify') diff --git a/homeassistant/components/tahoma.py b/homeassistant/components/tahoma.py index 1cbc81709c4819..aaa64489168671 100644 --- a/homeassistant/components/tahoma.py +++ b/homeassistant/components/tahoma.py @@ -50,6 +50,8 @@ 'io:WindowOpenerVeluxIOComponent': 'cover', 'io:LightIOSystemSensor': 'sensor', 'rts:GarageDoor4TRTSComponent': 'switch', + 'io:VerticalExteriorAwningIOComponent': 'cover', + 'io:HorizontalAwningIOComponent': 'cover', 'rtds:RTDSSmokeSensor': 'smoke', } From 8a81ee3b4ff3b725273186c5fe596e2f59cf2ce8 Mon Sep 17 00:00:00 2001 From: Thomas Delaet Date: Sun, 5 Aug 2018 11:47:17 +0300 Subject: [PATCH 043/117] Velbus auto-discovery (#13742) * remove velbus fan and light platforms these platforms should not be there since they can be created with template components based on switch platform * use latest version of python-velbus which supports auto-discovery of modules * fix linting errors * fix linting errors * fix linting errors * address review comments from @MartinHjelmare * update based on automatic feedback * fix linting errors * update dependency * syntax corrections * fix lint warning * split out common functionality in VelbusEntity use sync methods for loading platforms support unique_ids so that entities are registred in entity registry * fix linting errors * fix linting errors * fix linting errors * integrate review comments (common functionality in VelbusEntity class) * rename DOMAIN import to VELBUS_DOMAIN * revert change created by requirements script * regen --- .../components/binary_sensor/velbus.py | 89 ++------- homeassistant/components/fan/velbus.py | 187 ------------------ homeassistant/components/light/velbus.py | 104 ---------- homeassistant/components/switch/velbus.py | 102 ++-------- homeassistant/components/velbus.py | 72 ++++++- requirements_all.txt | 2 +- 6 files changed, 100 insertions(+), 456 deletions(-) delete mode 100644 homeassistant/components/fan/velbus.py delete mode 100644 homeassistant/components/light/velbus.py diff --git a/homeassistant/components/binary_sensor/velbus.py b/homeassistant/components/binary_sensor/velbus.py index 214edcf9463856..8438be0d7845df 100644 --- a/homeassistant/components/binary_sensor/velbus.py +++ b/homeassistant/components/binary_sensor/velbus.py @@ -4,93 +4,34 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/binary_sensor.velbus/ """ -import asyncio import logging - -import voluptuous as vol - -from homeassistant.const import CONF_NAME, CONF_DEVICES from homeassistant.components.binary_sensor import BinarySensorDevice -from homeassistant.components.binary_sensor import PLATFORM_SCHEMA -from homeassistant.components.velbus import DOMAIN -import homeassistant.helpers.config_validation as cv - - -DEPENDENCIES = ['velbus'] +from homeassistant.components.velbus import ( + DOMAIN as VELBUS_DOMAIN, VelbusEntity) _LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_DEVICES): vol.All(cv.ensure_list, [ - { - vol.Required('module'): cv.positive_int, - vol.Required('channel'): cv.positive_int, - vol.Required(CONF_NAME): cv.string, - vol.Optional('is_pushbutton'): cv.boolean - } - ]) -}) +DEPENDENCIES = ['velbus'] -def setup_platform(hass, config, add_devices, discovery_info=None): +async def async_setup_platform(hass, config, async_add_devices, + discovery_info=None): """Set up Velbus binary sensors.""" - velbus = hass.data[DOMAIN] + if discovery_info is None: + return + sensors = [] + for sensor in discovery_info: + module = hass.data[VELBUS_DOMAIN].get_module(sensor[0]) + channel = sensor[1] + sensors.append(VelbusBinarySensor(module, channel)) + async_add_devices(sensors) - add_devices(VelbusBinarySensor(sensor, velbus) - for sensor in config[CONF_DEVICES]) - -class VelbusBinarySensor(BinarySensorDevice): +class VelbusBinarySensor(VelbusEntity, BinarySensorDevice): """Representation of a Velbus Binary Sensor.""" - def __init__(self, binary_sensor, velbus): - """Initialize a Velbus light.""" - self._velbus = velbus - self._name = binary_sensor[CONF_NAME] - self._module = binary_sensor['module'] - self._channel = binary_sensor['channel'] - self._is_pushbutton = 'is_pushbutton' in binary_sensor \ - and binary_sensor['is_pushbutton'] - self._state = False - - @asyncio.coroutine - def async_added_to_hass(self): - """Add listener for Velbus messages on bus.""" - yield from self.hass.async_add_job( - self._velbus.subscribe, self._on_message) - - def _on_message(self, message): - import velbus - if isinstance(message, velbus.PushButtonStatusMessage): - if message.address == self._module and \ - self._channel in message.get_channels(): - if self._is_pushbutton: - if self._channel in message.closed: - self._toggle() - else: - pass - else: - self._toggle() - - def _toggle(self): - if self._state is True: - self._state = False - else: - self._state = True - self.schedule_update_ha_state() - - @property - def should_poll(self): - """No polling needed.""" - return False - - @property - def name(self): - """Return the display name of this sensor.""" - return self._name - @property def is_on(self): """Return true if the sensor is on.""" - return self._state + return self._module.is_closed(self._channel) diff --git a/homeassistant/components/fan/velbus.py b/homeassistant/components/fan/velbus.py deleted file mode 100644 index e8208d1c9907a9..00000000000000 --- a/homeassistant/components/fan/velbus.py +++ /dev/null @@ -1,187 +0,0 @@ -""" -Support for Velbus platform. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/fan.velbus/ -""" -import asyncio -import logging -import voluptuous as vol - -from homeassistant.components.fan import ( - SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH, FanEntity, SUPPORT_SET_SPEED, - PLATFORM_SCHEMA) -from homeassistant.components.velbus import DOMAIN -from homeassistant.const import CONF_NAME, CONF_DEVICES, STATE_OFF -import homeassistant.helpers.config_validation as cv - -DEPENDENCIES = ['velbus'] - -_LOGGER = logging.getLogger(__name__) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_DEVICES): vol.All(cv.ensure_list, [ - { - vol.Required('module'): cv.positive_int, - vol.Required('channel_low'): cv.positive_int, - vol.Required('channel_medium'): cv.positive_int, - vol.Required('channel_high'): cv.positive_int, - vol.Required(CONF_NAME): cv.string, - } - ]) -}) - - -def setup_platform(hass, config, add_devices, discovery_info=None): - """Set up Fans.""" - velbus = hass.data[DOMAIN] - add_devices(VelbusFan(fan, velbus) for fan in config[CONF_DEVICES]) - - -class VelbusFan(FanEntity): - """Representation of a Velbus Fan.""" - - def __init__(self, fan, velbus): - """Initialize a Velbus light.""" - self._velbus = velbus - self._name = fan[CONF_NAME] - self._module = fan['module'] - self._channel_low = fan['channel_low'] - self._channel_medium = fan['channel_medium'] - self._channel_high = fan['channel_high'] - self._channels = [self._channel_low, self._channel_medium, - self._channel_high] - self._channels_state = [False, False, False] - self._speed = STATE_OFF - - @asyncio.coroutine - def async_added_to_hass(self): - """Add listener for Velbus messages on bus.""" - def _init_velbus(): - """Initialize Velbus on startup.""" - self._velbus.subscribe(self._on_message) - self.get_status() - - yield from self.hass.async_add_job(_init_velbus) - - def _on_message(self, message): - import velbus - if isinstance(message, velbus.RelayStatusMessage) and \ - message.address == self._module and \ - message.channel in self._channels: - if message.channel == self._channel_low: - self._channels_state[0] = message.is_on() - elif message.channel == self._channel_medium: - self._channels_state[1] = message.is_on() - elif message.channel == self._channel_high: - self._channels_state[2] = message.is_on() - self._calculate_speed() - self.schedule_update_ha_state() - - def _calculate_speed(self): - if self._is_off(): - self._speed = STATE_OFF - elif self._is_low(): - self._speed = SPEED_LOW - elif self._is_medium(): - self._speed = SPEED_MEDIUM - elif self._is_high(): - self._speed = SPEED_HIGH - - def _is_off(self): - return self._channels_state[0] is False and \ - self._channels_state[1] is False and \ - self._channels_state[2] is False - - def _is_low(self): - return self._channels_state[0] is True and \ - self._channels_state[1] is False and \ - self._channels_state[2] is False - - def _is_medium(self): - return self._channels_state[0] is True and \ - self._channels_state[1] is True and \ - self._channels_state[2] is False - - def _is_high(self): - return self._channels_state[0] is True and \ - self._channels_state[1] is False and \ - self._channels_state[2] is True - - @property - def name(self): - """Return the display name of this light.""" - return self._name - - @property - def should_poll(self): - """Disable polling.""" - return False - - @property - def speed(self): - """Return the current speed.""" - return self._speed - - @property - def speed_list(self): - """Get the list of available speeds.""" - return [STATE_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH] - - def turn_on(self, speed=None, **kwargs): - """Turn on the entity.""" - if speed is None: - speed = SPEED_MEDIUM - self.set_speed(speed) - - def turn_off(self, **kwargs): - """Turn off the entity.""" - self.set_speed(STATE_OFF) - - def set_speed(self, speed): - """Set the speed of the fan.""" - channels_off = [] - channels_on = [] - if speed == STATE_OFF: - channels_off = self._channels - elif speed == SPEED_LOW: - channels_off = [self._channel_medium, self._channel_high] - channels_on = [self._channel_low] - elif speed == SPEED_MEDIUM: - channels_off = [self._channel_high] - channels_on = [self._channel_low, self._channel_medium] - elif speed == SPEED_HIGH: - channels_off = [self._channel_medium] - channels_on = [self._channel_low, self._channel_high] - for channel in channels_off: - self._relay_off(channel) - for channel in channels_on: - self._relay_on(channel) - self.schedule_update_ha_state() - - def _relay_on(self, channel): - import velbus - message = velbus.SwitchRelayOnMessage() - message.set_defaults(self._module) - message.relay_channels = [channel] - self._velbus.send(message) - - def _relay_off(self, channel): - import velbus - message = velbus.SwitchRelayOffMessage() - message.set_defaults(self._module) - message.relay_channels = [channel] - self._velbus.send(message) - - def get_status(self): - """Retrieve current status.""" - import velbus - message = velbus.ModuleStatusRequestMessage() - message.set_defaults(self._module) - message.channels = self._channels - self._velbus.send(message) - - @property - def supported_features(self): - """Flag supported features.""" - return SUPPORT_SET_SPEED diff --git a/homeassistant/components/light/velbus.py b/homeassistant/components/light/velbus.py deleted file mode 100644 index 8a02b36b75faad..00000000000000 --- a/homeassistant/components/light/velbus.py +++ /dev/null @@ -1,104 +0,0 @@ -""" -Support for Velbus lights. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/light.velbus/ -""" -import asyncio -import logging - -import voluptuous as vol - -from homeassistant.const import CONF_NAME, CONF_DEVICES -from homeassistant.components.light import Light, PLATFORM_SCHEMA -from homeassistant.components.velbus import DOMAIN -import homeassistant.helpers.config_validation as cv - -DEPENDENCIES = ['velbus'] - -_LOGGER = logging.getLogger(__name__) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_DEVICES): vol.All(cv.ensure_list, [ - { - vol.Required('module'): cv.positive_int, - vol.Required('channel'): cv.positive_int, - vol.Required(CONF_NAME): cv.string - } - ]) -}) - - -def setup_platform(hass, config, add_devices, discovery_info=None): - """Set up Lights.""" - velbus = hass.data[DOMAIN] - add_devices(VelbusLight(light, velbus) for light in config[CONF_DEVICES]) - - -class VelbusLight(Light): - """Representation of a Velbus Light.""" - - def __init__(self, light, velbus): - """Initialize a Velbus light.""" - self._velbus = velbus - self._name = light[CONF_NAME] - self._module = light['module'] - self._channel = light['channel'] - self._state = False - - @asyncio.coroutine - def async_added_to_hass(self): - """Add listener for Velbus messages on bus.""" - def _init_velbus(): - """Initialize Velbus on startup.""" - self._velbus.subscribe(self._on_message) - self.get_status() - - yield from self.hass.async_add_job(_init_velbus) - - def _on_message(self, message): - import velbus - if isinstance(message, velbus.RelayStatusMessage) and \ - message.address == self._module and \ - message.channel == self._channel: - self._state = message.is_on() - self.schedule_update_ha_state() - - @property - def name(self): - """Return the display name of this light.""" - return self._name - - @property - def should_poll(self): - """Disable polling.""" - return False - - @property - def is_on(self): - """Return true if the light is on.""" - return self._state - - def turn_on(self, **kwargs): - """Instruct the light to turn on.""" - import velbus - message = velbus.SwitchRelayOnMessage() - message.set_defaults(self._module) - message.relay_channels = [self._channel] - self._velbus.send(message) - - def turn_off(self, **kwargs): - """Instruct the light to turn off.""" - import velbus - message = velbus.SwitchRelayOffMessage() - message.set_defaults(self._module) - message.relay_channels = [self._channel] - self._velbus.send(message) - - def get_status(self): - """Retrieve current status.""" - import velbus - message = velbus.ModuleStatusRequestMessage() - message.set_defaults(self._module) - message.channels = [self._channel] - self._velbus.send(message) diff --git a/homeassistant/components/switch/velbus.py b/homeassistant/components/switch/velbus.py index 15090091a52480..46f6e893c9714d 100644 --- a/homeassistant/components/switch/velbus.py +++ b/homeassistant/components/switch/velbus.py @@ -4,108 +4,42 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/switch.velbus/ """ - -import asyncio import logging -import voluptuous as vol - -from homeassistant.const import CONF_NAME, CONF_DEVICES -from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA -from homeassistant.components.velbus import DOMAIN -import homeassistant.helpers.config_validation as cv +from homeassistant.components.switch import SwitchDevice +from homeassistant.components.velbus import ( + DOMAIN as VELBUS_DOMAIN, VelbusEntity) _LOGGER = logging.getLogger(__name__) -SWITCH_SCHEMA = { - vol.Required('module'): cv.positive_int, - vol.Required('channel'): cv.positive_int, - vol.Required(CONF_NAME): cv.string -} - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_DEVICES): - vol.All(cv.ensure_list, [SWITCH_SCHEMA]) -}) - DEPENDENCIES = ['velbus'] -def setup_platform(hass, config, add_devices, discovery_info=None): - """Set up the Switch.""" - velbus = hass.data[DOMAIN] - devices = [] +async def async_setup_platform(hass, config, async_add_devices, + discovery_info=None): + """Set up the Velbus Switch platform.""" + if discovery_info is None: + return + switches = [] + for switch in discovery_info: + module = hass.data[VELBUS_DOMAIN].get_module(switch[0]) + channel = switch[1] + switches.append(VelbusSwitch(module, channel)) + async_add_devices(switches) - for switch in config[CONF_DEVICES]: - devices.append(VelbusSwitch(switch, velbus)) - add_devices(devices) - return True - -class VelbusSwitch(SwitchDevice): +class VelbusSwitch(VelbusEntity, SwitchDevice): """Representation of a switch.""" - def __init__(self, switch, velbus): - """Initialize a Velbus switch.""" - self._velbus = velbus - self._name = switch[CONF_NAME] - self._module = switch['module'] - self._channel = switch['channel'] - self._state = False - - @asyncio.coroutine - def async_added_to_hass(self): - """Add listener for Velbus messages on bus.""" - def _init_velbus(): - """Initialize Velbus on startup.""" - self._velbus.subscribe(self._on_message) - self.get_status() - - yield from self.hass.async_add_job(_init_velbus) - - def _on_message(self, message): - import velbus - if isinstance(message, velbus.RelayStatusMessage) and \ - message.address == self._module and \ - message.channel == self._channel: - self._state = message.is_on() - self.schedule_update_ha_state() - - @property - def name(self): - """Return the display name of this switch.""" - return self._name - - @property - def should_poll(self): - """Disable polling.""" - return False - @property def is_on(self): """Return true if the switch is on.""" - return self._state + return self._module.is_on(self._channel) def turn_on(self, **kwargs): """Instruct the switch to turn on.""" - import velbus - message = velbus.SwitchRelayOnMessage() - message.set_defaults(self._module) - message.relay_channels = [self._channel] - self._velbus.send(message) + self._module.turn_on(self._channel) def turn_off(self, **kwargs): """Instruct the switch to turn off.""" - import velbus - message = velbus.SwitchRelayOffMessage() - message.set_defaults(self._module) - message.relay_channels = [self._channel] - self._velbus.send(message) - - def get_status(self): - """Retrieve current status.""" - import velbus - message = velbus.ModuleStatusRequestMessage() - message.set_defaults(self._module) - message.channels = [self._channel] - self._velbus.send(message) + self._module.turn_off(self._channel) diff --git a/homeassistant/components/velbus.py b/homeassistant/components/velbus.py index ff2db955d31aae..8c9449169058a8 100644 --- a/homeassistant/components/velbus.py +++ b/homeassistant/components/velbus.py @@ -9,8 +9,10 @@ import homeassistant.helpers.config_validation as cv from homeassistant.const import EVENT_HOMEASSISTANT_STOP, CONF_PORT +from homeassistant.helpers.discovery import load_platform +from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['python-velbus==2.0.11'] +REQUIREMENTS = ['python-velbus==2.0.17'] _LOGGER = logging.getLogger(__name__) @@ -26,18 +28,76 @@ }, extra=vol.ALLOW_EXTRA) -def setup(hass, config): +async def async_setup(hass, config): """Set up the Velbus platform.""" import velbus port = config[DOMAIN].get(CONF_PORT) - connection = velbus.VelbusUSBConnection(port) - controller = velbus.Controller(connection) + controller = velbus.Controller(port) + hass.data[DOMAIN] = controller def stop_velbus(event): """Disconnect from serial port.""" _LOGGER.debug("Shutting down ") - connection.stop() + controller.stop() + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_velbus) + + def callback(): + modules = controller.get_modules() + discovery_info = { + 'switch': [], + 'binary_sensor': [] + } + for module in modules: + for channel in range(1, module.number_of_channels() + 1): + for category in discovery_info: + if category in module.get_categories(channel): + discovery_info[category].append(( + module.get_module_address(), + channel + )) + load_platform(hass, 'switch', DOMAIN, + discovery_info['switch'], config) + load_platform(hass, 'binary_sensor', DOMAIN, + discovery_info['binary_sensor'], config) + + controller.scan(callback) - hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_velbus) return True + + +class VelbusEntity(Entity): + """Representation of a Velbus entity.""" + + def __init__(self, module, channel): + """Initialize a Velbus entity.""" + self._module = module + self._channel = channel + + @property + def unique_id(self): + """Get unique ID.""" + serial = 0 + if self._module.serial == 0: + serial = self._module.get_module_address() + else: + serial = self._module.serial + return "{}-{}".format(serial, self._channel) + + @property + def name(self): + """Return the display name of this entity.""" + return self._module.get_name(self._channel) + + @property + def should_poll(self): + """Disable polling.""" + return False + + async def async_added_to_hass(self): + """Add listener for state changes.""" + self._module.on_status_update(self._channel, self._on_update) + + def _on_update(self, state): + self.schedule_update_ha_state() diff --git a/requirements_all.txt b/requirements_all.txt index 16beb1ef4a3af9..636f701f2b3a39 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1117,7 +1117,7 @@ python-telegram-bot==10.1.0 python-twitch==1.3.0 # homeassistant.components.velbus -python-velbus==2.0.11 +python-velbus==2.0.17 # homeassistant.components.media_player.vlc python-vlc==1.1.2 From c41aa12d1dd1c6d309e8a3a869522591d109e759 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 5 Aug 2018 13:29:06 +0200 Subject: [PATCH 044/117] Upgrade youtube_dl to 2018.08.04 (#15837) --- 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 029d10ea00a14d..793d33e52fafb5 100644 --- a/homeassistant/components/media_extractor.py +++ b/homeassistant/components/media_extractor.py @@ -14,7 +14,7 @@ SERVICE_PLAY_MEDIA) from homeassistant.helpers import config_validation as cv -REQUIREMENTS = ['youtube_dl==2018.07.29'] +REQUIREMENTS = ['youtube_dl==2018.08.04'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 636f701f2b3a39..cd68a5c32594f2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1475,7 +1475,7 @@ yeelight==0.4.0 yeelightsunflower==0.0.10 # homeassistant.components.media_extractor -youtube_dl==2018.07.29 +youtube_dl==2018.08.04 # homeassistant.components.light.zengge zengge==0.2 From b152becbe0d696cc840f52aa755772beb1bd2de4 Mon Sep 17 00:00:00 2001 From: Steven Looman Date: Sun, 5 Aug 2018 14:41:18 +0200 Subject: [PATCH 045/117] Add media_player.dlna_dmr component (#14749) * Add media_player.dlna_dmr component * PEP 492 * Move DIDL-template up * Remove max_volume-override option * Remove picky_device support * Use DEFAULT_NAME * Make supported_features static * Remove unneeded argument * Proper module-docstring * Add http dependency * Remove additional_configuration options, no longer used * Change default name to 'DLNA Digital Media Renderer' * Use python-didl-lite for DIDL-Lite-xml construction/parsing * Handle NOT_IMPLEMENTED for UPnP state variables RelativeTimePosition and CurrentMediaDuration * Use UPnP-UDN for unique_id * Proper handling of upnp events * Keeping flake8 happy * Update requirements_all.txt * Make UDN optional * Ensure NotifyView is started, before using it * Only subscribe to services we're interested in * Don't update state_variables if value has not been changed + minor refactoring * Improve play_media, follow flow of DLNA more closely * Hopefully fix ClientOSError problems * Flake8 fixes * Keep pylint happy * Catch errors and report gracefully * Update async_upnp_client to 0.11.0 * Don't be so noisy * Define/use constants for HTTP status codes * Add discovery entry for dlna_dmr * More robustness with regard to state variable not being set (yet) * Keep privates hidden * Handle NOT_IMPLEMENTED for CurrentTrackMetaData state variable * Fixes in async_upnp_client + renew UPnP subscriptions regularly * Not too eager * Refactor duplicate code to _current_transport_actions and improve parsing of actions * Support RC:1 to RC:3 and AVT:1 to AVT:3 * Moved DLNA-specifics to async_upnp_client.dlna.DmrDevice * Use our own HTTP server to listen for events. * More clear and explicit log message for easier troubleshooting * Follow changes by hass, fixes traceback * Fix not being able to do next * Changes after review by @MartinHjelmare * Linting * Use homeassistant.util.get_local_ip * Moved upnp event handling to async_upnp_client * Keeping pylint happy * Changes after review by @MartinHjelmare --- .coveragerc | 1 + homeassistant/components/discovery.py | 1 + .../components/media_player/dlna_dmr.py | 414 ++++++++++++++++++ requirements_all.txt | 3 + 4 files changed, 419 insertions(+) create mode 100644 homeassistant/components/media_player/dlna_dmr.py diff --git a/.coveragerc b/.coveragerc index f42bd5cd3db6e7..1e358cd779158d 100644 --- a/.coveragerc +++ b/.coveragerc @@ -513,6 +513,7 @@ omit = homeassistant/components/media_player/denon.py homeassistant/components/media_player/denonavr.py homeassistant/components/media_player/directv.py + homeassistant/components/media_player/dlna_dmr.py homeassistant/components/media_player/dunehd.py homeassistant/components/media_player/emby.py homeassistant/components/media_player/epson.py diff --git a/homeassistant/components/discovery.py b/homeassistant/components/discovery.py index 78b891bae922ae..8877f05f622587 100644 --- a/homeassistant/components/discovery.py +++ b/homeassistant/components/discovery.py @@ -85,6 +85,7 @@ 'volumio': ('media_player', 'volumio'), 'nanoleaf_aurora': ('light', 'nanoleaf_aurora'), 'freebox': ('device_tracker', 'freebox'), + 'DLNA': ('media_player', 'dlna_dmr') } OPTIONAL_SERVICE_HANDLERS = { diff --git a/homeassistant/components/media_player/dlna_dmr.py b/homeassistant/components/media_player/dlna_dmr.py new file mode 100644 index 00000000000000..98cd865b703f2d --- /dev/null +++ b/homeassistant/components/media_player/dlna_dmr.py @@ -0,0 +1,414 @@ +# -*- coding: utf-8 -*- +""" +Support for DLNA DMR (Device Media Renderer). + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/media_player.dlna_dmr/ +""" + +import asyncio +import functools +import logging +from datetime import datetime + +import aiohttp +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.media_player import ( + SUPPORT_PLAY, SUPPORT_PAUSE, SUPPORT_STOP, + SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, + SUPPORT_PLAY_MEDIA, + SUPPORT_PREVIOUS_TRACK, SUPPORT_NEXT_TRACK, + MediaPlayerDevice, + PLATFORM_SCHEMA) +from homeassistant.const import ( + EVENT_HOMEASSISTANT_STOP, + CONF_URL, CONF_NAME, + STATE_OFF, STATE_ON, STATE_IDLE, STATE_PLAYING, STATE_PAUSED) +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import PlatformNotReady +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.util import get_local_ip + + +DLNA_DMR_DATA = 'dlna_dmr' + +REQUIREMENTS = [ + 'async-upnp-client==0.12.2', +] + +DEFAULT_NAME = 'DLNA Digital Media Renderer' +DEFAULT_LISTEN_PORT = 8301 + +CONF_LISTEN_IP = 'listen_ip' +CONF_LISTEN_PORT = 'listen_port' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_URL): cv.string, + vol.Optional(CONF_LISTEN_IP): cv.string, + vol.Optional(CONF_LISTEN_PORT, default=DEFAULT_LISTEN_PORT): cv.port, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, +}) + +HOME_ASSISTANT_UPNP_CLASS_MAPPING = { + 'music': 'object.item.audioItem', + 'tvshow': 'object.item.videoItem', + 'video': 'object.item.videoItem', + 'episode': 'object.item.videoItem', + 'channel': 'object.item.videoItem', + 'playlist': 'object.item.playlist', +} +HOME_ASSISTANT_UPNP_MIME_TYPE_MAPPING = { + 'music': 'audio/*', + 'tvshow': 'video/*', + 'video': 'video/*', + 'episode': 'video/*', + 'channel': 'video/*', + 'playlist': 'playlist/*', +} +UPNP_DEVICE_MEDIA_RENDERER = [ + 'urn:schemas-upnp-org:device:MediaRenderer:1', + 'urn:schemas-upnp-org:device:MediaRenderer:2', + 'urn:schemas-upnp-org:device:MediaRenderer:3', +] + +_LOGGER = logging.getLogger(__name__) + + +def catch_request_errors(): + """Catch asyncio.TimeoutError, aiohttp.ClientError errors.""" + def call_wrapper(func): + """Call wrapper for decorator.""" + @functools.wraps(func) + def wrapper(self, *args, **kwargs): + """Catch asyncio.TimeoutError, aiohttp.ClientError errors.""" + try: + return func(self, *args, **kwargs) + except (asyncio.TimeoutError, aiohttp.ClientError): + _LOGGER.error("Error during call %s", func.__name__) + + return wrapper + + return call_wrapper + + +async def async_start_event_handler(hass, server_host, server_port, requester): + """Register notify view.""" + hass_data = hass.data[DLNA_DMR_DATA] + if 'event_handler' in hass_data: + return hass_data['event_handler'] + + # start event handler + from async_upnp_client.aiohttp import AiohttpNotifyServer + server = AiohttpNotifyServer(requester, + server_port, + server_host, + hass.loop) + await server.start_server() + _LOGGER.info('UPNP/DLNA event handler listening on: %s', + server.callback_url) + hass_data['notify_server'] = server + hass_data['event_handler'] = server.event_handler + + # register for graceful shutdown + async def async_stop_server(event): + """Stop server.""" + _LOGGER.debug('Stopping UPNP/DLNA event handler') + await server.stop_server() + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_stop_server) + + return hass_data['event_handler'] + + +async def async_setup_platform(hass: HomeAssistant, + config, + async_add_devices, + discovery_info=None): + """Set up DLNA DMR platform.""" + # ensure this is a DLNA DMR device, if found via discovery + if discovery_info and \ + 'upnp_device_type' in discovery_info and \ + discovery_info['upnp_device_type'] not in UPNP_DEVICE_MEDIA_RENDERER: + _LOGGER.debug('Device is not a MediaRenderer: %s, device_type: %s', + discovery_info.get('ssdp_description'), + discovery_info['upnp_device_type']) + return + + if config.get(CONF_URL) is not None: + url = config[CONF_URL] + name = config.get(CONF_NAME) + elif discovery_info is not None: + url = discovery_info['ssdp_description'] + name = discovery_info['name'] + + if DLNA_DMR_DATA not in hass.data: + hass.data[DLNA_DMR_DATA] = {} + + if 'lock' not in hass.data[DLNA_DMR_DATA]: + hass.data[DLNA_DMR_DATA]['lock'] = asyncio.Lock() + + # build upnp/aiohttp requester + from async_upnp_client.aiohttp import AiohttpSessionRequester + session = async_get_clientsession(hass) + requester = AiohttpSessionRequester(session, True) + + # ensure event handler has been started + with await hass.data[DLNA_DMR_DATA]['lock']: + server_host = config.get(CONF_LISTEN_IP) + if server_host is None: + server_host = get_local_ip() + server_port = config.get(CONF_LISTEN_PORT, DEFAULT_LISTEN_PORT) + event_handler = await async_start_event_handler(hass, + server_host, + server_port, + requester) + + # 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 DmrDevice + from async_upnp_client.dlna import DmrDevice + dlna_device = DmrDevice(upnp_device, event_handler) + + # create our own device + device = DlnaDmrDevice(dlna_device, name) + _LOGGER.debug("Adding device: %s", device) + async_add_devices([device], True) + + +class DlnaDmrDevice(MediaPlayerDevice): + """Representation of a DLNA DMR device.""" + + def __init__(self, dmr_device, name=None): + """Initializer.""" + self._device = dmr_device + self._name = name + + self._available = False + self._subscription_renew_time = None + + async def async_added_to_hass(self): + """Callback when added.""" + self._device.on_event = self._on_event + + # register unsubscribe on stop + bus = self.hass.bus + bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, + self._async_on_hass_stop) + + @property + def available(self): + """Device is available.""" + return self._available + + async def _async_on_hass_stop(self, event): + """Event handler on HASS stop.""" + with await self.hass.data[DLNA_DMR_DATA]['lock']: + await self._device.async_unsubscribe_services() + + async def async_update(self): + """Retrieve the latest data.""" + was_available = self._available + + try: + await self._device.async_update() + self._available = True + except (asyncio.TimeoutError, aiohttp.ClientError): + self._available = False + _LOGGER.debug("Device unavailable") + return + + # do we need to (re-)subscribe? + now = datetime.now() + should_renew = self._subscription_renew_time and \ + now >= self._subscription_renew_time + if should_renew or \ + not was_available and self._available: + try: + timeout = await self._device.async_subscribe_services() + self._subscription_renew_time = datetime.now() + timeout / 2 + except (asyncio.TimeoutError, aiohttp.ClientError): + self._available = False + _LOGGER.debug("Could not (re)subscribe") + + def _on_event(self, service, state_variables): + """State variable(s) changed, let home-assistant know.""" + self.schedule_update_ha_state() + + @property + def supported_features(self): + """Flag media player features that are supported.""" + supported_features = 0 + + if self._device.has_volume_level: + supported_features |= SUPPORT_VOLUME_SET + if self._device.has_volume_mute: + supported_features |= SUPPORT_VOLUME_MUTE + if self._device.has_play: + supported_features |= SUPPORT_PLAY + if self._device.has_pause: + supported_features |= SUPPORT_PAUSE + if self._device.has_stop: + supported_features |= SUPPORT_STOP + if self._device.has_previous: + supported_features |= SUPPORT_PREVIOUS_TRACK + if self._device.has_next: + supported_features |= SUPPORT_NEXT_TRACK + if self._device.has_play_media: + supported_features |= SUPPORT_PLAY_MEDIA + + return supported_features + + @property + def volume_level(self): + """Volume level of the media player (0..1).""" + return self._device.volume_level + + @catch_request_errors() + async def async_set_volume_level(self, volume): + """Set volume level, range 0..1.""" + await self._device.async_set_volume_level(volume) + + @property + def is_volume_muted(self): + """Boolean if volume is currently muted.""" + return self._device.is_volume_muted + + @catch_request_errors() + async def async_mute_volume(self, mute): + """Mute the volume.""" + desired_mute = bool(mute) + await self._device.async_mute_volume(desired_mute) + + @catch_request_errors() + async def async_media_pause(self): + """Send pause command.""" + if not self._device.can_pause: + _LOGGER.debug('Cannot do Pause') + return + + await self._device.async_pause() + + @catch_request_errors() + async def async_media_play(self): + """Send play command.""" + if not self._device.can_play: + _LOGGER.debug('Cannot do Play') + return + + await self._device.async_play() + + @catch_request_errors() + async def async_media_stop(self): + """Send stop command.""" + if not self._device.can_stop: + _LOGGER.debug('Cannot do Stop') + return + + await self._device.async_stop() + + @catch_request_errors() + async def async_play_media(self, media_type, media_id, **kwargs): + """Play a piece of media.""" + title = "Home Assistant" + mime_type = HOME_ASSISTANT_UPNP_MIME_TYPE_MAPPING[media_type] + upnp_class = HOME_ASSISTANT_UPNP_CLASS_MAPPING[media_type] + + # stop current playing media + if self._device.can_stop: + await self.async_media_stop() + + # queue media + await self._device.async_set_transport_uri(media_id, + title, + mime_type, + upnp_class) + await self._device.async_wait_for_can_play() + + # if already playing, no need to call Play + from async_upnp_client import dlna + if self._device.state == dlna.STATE_PLAYING: + return + + # play it + await self.async_media_play() + + @catch_request_errors() + async def async_media_previous_track(self): + """Send previous track command.""" + if not self._device.can_previous: + _LOGGER.debug('Cannot do Previous') + return + + await self._device.async_previous() + + @catch_request_errors() + async def async_media_next_track(self): + """Send next track command.""" + if not self._device.can_next: + _LOGGER.debug('Cannot do Next') + return + + await self._device.async_next() + + @property + def media_title(self): + """Title of current playing media.""" + return self._device.media_title + + @property + def media_image_url(self): + """Image url of current playing media.""" + return self._device.media_image_url + + @property + def state(self): + """State of the player.""" + if not self._available: + return STATE_OFF + + from async_upnp_client import dlna + if self._device.state is None: + return STATE_ON + if self._device.state == dlna.STATE_PLAYING: + return STATE_PLAYING + if self._device.state == dlna.STATE_PAUSED: + return STATE_PAUSED + + return STATE_IDLE + + @property + def media_duration(self): + """Duration of current playing media in seconds.""" + return self._device.media_duration + + @property + def media_position(self): + """Position of current playing media in seconds.""" + return self._device.media_position + + @property + def media_position_updated_at(self): + """When was the position of the current playing media valid. + + Returns value from homeassistant.util.dt.utcnow(). + """ + return self._device.media_position_updated_at + + @property + def name(self) -> str: + """Return the name of the device.""" + if self._name: + return self._name + return self._device.name + + @property + def unique_id(self) -> str: + """Return an unique ID.""" + return self._device.udn diff --git a/requirements_all.txt b/requirements_all.txt index cd68a5c32594f2..2c7873175f467b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -135,6 +135,9 @@ apns2==0.3.0 # homeassistant.components.asterisk_mbox asterisk_mbox==0.4.0 +# homeassistant.components.media_player.dlna_dmr +async-upnp-client==0.12.2 + # homeassistant.components.light.avion # avion==0.7 From 6a32b9bf87b3b3d68dbd9a43d31cec36423c27e6 Mon Sep 17 00:00:00 2001 From: Dan Cinnamon Date: Sun, 5 Aug 2018 11:51:23 -0500 Subject: [PATCH 046/117] Fix envisalink reconnect (#15832) * Fix logic for handling connection lost/reconnect * Fixed line length issue. --- homeassistant/components/envisalink.py | 14 +++++++++----- requirements_all.txt | 2 +- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/envisalink.py b/homeassistant/components/envisalink.py index 7dd4e7dc32ae6b..9b5b25c934cfe3 100644 --- a/homeassistant/components/envisalink.py +++ b/homeassistant/components/envisalink.py @@ -16,7 +16,7 @@ from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.dispatcher import async_dispatcher_send -REQUIREMENTS = ['pyenvisalink==2.2'] +REQUIREMENTS = ['pyenvisalink==2.3'] _LOGGER = logging.getLogger(__name__) @@ -111,20 +111,24 @@ def async_setup(hass, config): def login_fail_callback(data): """Handle when the evl rejects our login.""" _LOGGER.error("The Envisalink rejected your credentials") - sync_connect.set_result(False) + if not sync_connect.done(): + sync_connect.set_result(False) @callback def connection_fail_callback(data): """Network failure callback.""" _LOGGER.error("Could not establish a connection with the Envisalink") - sync_connect.set_result(False) + if not sync_connect.done(): + sync_connect.set_result(False) @callback def connection_success_callback(data): """Handle a successful connection.""" _LOGGER.info("Established a connection with the Envisalink") - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_envisalink) - sync_connect.set_result(True) + if not sync_connect.done(): + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, + stop_envisalink) + sync_connect.set_result(True) @callback def zones_updated_callback(data): diff --git a/requirements_all.txt b/requirements_all.txt index 2c7873175f467b..19da051c4522d2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -826,7 +826,7 @@ pyeight==0.0.9 pyemby==1.5 # homeassistant.components.envisalink -pyenvisalink==2.2 +pyenvisalink==2.3 # homeassistant.components.climate.ephember pyephember==0.1.1 From 9a84f8b7630bf54e290f8233bf09bce145198744 Mon Sep 17 00:00:00 2001 From: mattwing <1054287+mattwing@users.noreply.github.com> Date: Sun, 5 Aug 2018 16:11:51 -0400 Subject: [PATCH 047/117] Remove 'volume' from return dict (#15842) https://github.com/home-assistant/home-assistant/issues/15271 intraday results do not return the volume. See https://www.alphavantage.co/documentation/#intraday --- homeassistant/components/sensor/alpha_vantage.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/homeassistant/components/sensor/alpha_vantage.py b/homeassistant/components/sensor/alpha_vantage.py index 77d8ba9322f826..a7e6f6d26221bb 100644 --- a/homeassistant/components/sensor/alpha_vantage.py +++ b/homeassistant/components/sensor/alpha_vantage.py @@ -22,7 +22,6 @@ ATTR_CLOSE = 'close' ATTR_HIGH = 'high' ATTR_LOW = 'low' -ATTR_VOLUME = 'volume' CONF_ATTRIBUTION = "Stock market information provided by Alpha Vantage" CONF_FOREIGN_EXCHANGE = 'foreign_exchange' @@ -148,7 +147,6 @@ def device_state_attributes(self): ATTR_CLOSE: self.values['4. close'], ATTR_HIGH: self.values['2. high'], ATTR_LOW: self.values['3. low'], - ATTR_VOLUME: self.values['5. volume'], } @property From f86702e8abe2d10af3b58e9b64d43a92e5a2a92b Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 5 Aug 2018 22:48:14 +0200 Subject: [PATCH 048/117] Upgrade shodan to 1.9.0 (#15839) --- homeassistant/components/sensor/shodan.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/shodan.py b/homeassistant/components/sensor/shodan.py index 541abea3091b84..dfc49ce6639879 100644 --- a/homeassistant/components/sensor/shodan.py +++ b/homeassistant/components/sensor/shodan.py @@ -14,7 +14,7 @@ from homeassistant.const import ATTR_ATTRIBUTION, CONF_API_KEY, CONF_NAME from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['shodan==1.8.1'] +REQUIREMENTS = ['shodan==1.9.0'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 19da051c4522d2..5345694c379345 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1262,7 +1262,7 @@ sense_energy==0.3.1 sharp_aquos_rc==0.3.2 # homeassistant.components.sensor.shodan -shodan==1.8.1 +shodan==1.9.0 # homeassistant.components.notify.simplepush simplepush==1.1.4 From ac4674fdb0096cd2054182d67eaaabd91720c621 Mon Sep 17 00:00:00 2001 From: Ryan Davies Date: Mon, 6 Aug 2018 17:17:21 +1200 Subject: [PATCH 049/117] Add max_gps_accuracy option to Google Maps (#15833) * Google Maps - Add max_gps_accuracy option * Remove else statement and add continue --- .../components/device_tracker/google_maps.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/homeassistant/components/device_tracker/google_maps.py b/homeassistant/components/device_tracker/google_maps.py index d0669ab4967da8..1aeeb0e1030886 100644 --- a/homeassistant/components/device_tracker/google_maps.py +++ b/homeassistant/components/device_tracker/google_maps.py @@ -21,6 +21,7 @@ _LOGGER = logging.getLogger(__name__) +CONF_MAX_GPS_ACCURACY = 'max_gps_accuracy' ATTR_ADDRESS = 'address' ATTR_FULL_NAME = 'full_name' ATTR_LAST_SEEN = 'last_seen' @@ -31,6 +32,7 @@ MIN_TIME_BETWEEN_SCANS = timedelta(seconds=30) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_MAX_GPS_ACCURACY, default=100000): vol.Coerce(float), vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, }) @@ -53,6 +55,7 @@ def __init__(self, hass, config: ConfigType, see) -> None: self.see = see self.username = config[CONF_USERNAME] self.password = config[CONF_PASSWORD] + self.max_gps_accuracy = config[CONF_MAX_GPS_ACCURACY] try: self.service = Service(self.username, self.password, @@ -76,6 +79,14 @@ def _update_info(self, now=None): _LOGGER.warning("No location(s) shared with this account") return + if self.max_gps_accuracy is not None and \ + person.accuracy > self.max_gps_accuracy: + _LOGGER.info("Ignoring %s update because expected GPS " + "accuracy %s is not met: %s", + person.nickname, self.max_gps_accuracy, + person.accuracy) + continue + attrs = { ATTR_ADDRESS: person.address, ATTR_FULL_NAME: person.full_name, From e4b2ae29bddadf42dfde8bbb9b2332a8f7c1c883 Mon Sep 17 00:00:00 2001 From: ahobsonsayers <32173585+ahobsonsayers@users.noreply.github.com> Date: Mon, 6 Aug 2018 06:38:02 +0100 Subject: [PATCH 050/117] Fix bt_home_hub_5 device tracker (#15096) * Fix bt_home_hub_5 device tracker Updated BT Home Hub 5 device tracker component to get it working again. The old parsing method of the DNS table has been broken for a while causing the component to fail to get connected devices. A new parsing method has been implemened and fixes all previous issues. * Moved part of code to a published PyPi library * Fixed Violations * Fixed bugs in device tracker * Moved API Specific Code to PyPi Repository * Updated to fit requested changes, removed test as it is no longer valid and updated requirement_all.txt * Update to fit style requirements and remove redundant code * Removed Unnecessary Comment --- .../device_tracker/bt_home_hub_5.py | 85 ++++--------------- requirements_all.txt | 3 + .../device_tracker/test_bt_home_hub_5.py | 53 ------------ 3 files changed, 21 insertions(+), 120 deletions(-) delete mode 100644 tests/components/device_tracker/test_bt_home_hub_5.py diff --git a/homeassistant/components/device_tracker/bt_home_hub_5.py b/homeassistant/components/device_tracker/bt_home_hub_5.py index 93bc9270650f11..21c41df3a1d2ba 100644 --- a/homeassistant/components/device_tracker/bt_home_hub_5.py +++ b/homeassistant/components/device_tracker/bt_home_hub_5.py @@ -5,24 +5,22 @@ https://home-assistant.io/components/device_tracker.bt_home_hub_5/ """ import logging -import re -import xml.etree.ElementTree as ET -import json -from urllib.parse import unquote -import requests import voluptuous as vol import homeassistant.helpers.config_validation as cv -from homeassistant.components.device_tracker import ( - DOMAIN, PLATFORM_SCHEMA, DeviceScanner) +from homeassistant.components.device_tracker import (DOMAIN, PLATFORM_SCHEMA, + DeviceScanner) from homeassistant.const import CONF_HOST +REQUIREMENTS = ['bthomehub5-devicelist==0.1.1'] + _LOGGER = logging.getLogger(__name__) -_MAC_REGEX = re.compile(r'(([0-9A-Fa-f]{1,2}\:){5}[0-9A-Fa-f]{1,2})') + +CONF_DEFAULT_IP = '192.168.1.254' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_HOST): cv.string + vol.Optional(CONF_HOST, default=CONF_DEFAULT_IP): cv.string, }) @@ -38,18 +36,19 @@ class BTHomeHub5DeviceScanner(DeviceScanner): def __init__(self, config): """Initialise the scanner.""" + import bthomehub5_devicelist + _LOGGER.info("Initialising BT Home Hub 5") - self.host = config.get(CONF_HOST, '192.168.1.254') + self.host = config[CONF_HOST] self.last_results = {} - self.url = 'http://{}/nonAuth/home_status.xml'.format(self.host) # Test the router is accessible - data = _get_homehub_data(self.url) + data = bthomehub5_devicelist.get_devicelist(self.host) self.success_init = data is not None def scan_devices(self): """Scan for new devices and return a list with found device IDs.""" - self._update_info() + self.update_info() return (device for device in self.last_results) @@ -57,71 +56,23 @@ def get_device_name(self, device): """Return the name of the given device or None if we don't know.""" # If not initialised and not already scanned and not found. if device not in self.last_results: - self._update_info() + self.update_info() if not self.last_results: return None return self.last_results.get(device) - def _update_info(self): - """Ensure the information from the BT Home Hub 5 is up to date. - - Return boolean if scanning successful. - """ - if not self.success_init: - return False + def update_info(self): + """Ensure the information from the BT Home Hub 5 is up to date.""" + import bthomehub5_devicelist _LOGGER.info("Scanning") - data = _get_homehub_data(self.url) + data = bthomehub5_devicelist.get_devicelist(self.host) if not data: _LOGGER.warning("Error scanning devices") - return False + return self.last_results = data - - return True - - -def _get_homehub_data(url): - """Retrieve data from BT Home Hub 5 and return parsed result.""" - try: - response = requests.get(url, timeout=5) - except requests.exceptions.Timeout: - _LOGGER.exception("Connection to the router timed out") - return - if response.status_code == 200: - return _parse_homehub_response(response.text) - _LOGGER.error("Invalid response from Home Hub: %s", response) - - -def _parse_homehub_response(data_str): - """Parse the BT Home Hub 5 data format.""" - root = ET.fromstring(data_str) - - dirty_json = root.find('known_device_list').get('value') - - # Normalise the JavaScript data to JSON. - clean_json = unquote(dirty_json.replace('\'', '\"') - .replace('{', '{\"') - .replace(':\"', '\":\"') - .replace('\",', '\",\"')) - - known_devices = [x for x in json.loads(clean_json) if x] - - devices = {} - - for device in known_devices: - name = device.get('name') - mac = device.get('mac') - - if _MAC_REGEX.match(mac) or ',' in mac: - for mac_addr in mac.split(','): - if _MAC_REGEX.match(mac_addr): - devices[mac_addr] = name - else: - devices[mac] = name - - return devices diff --git a/requirements_all.txt b/requirements_all.txt index 5345694c379345..2ee58f9033471c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -208,6 +208,9 @@ brunt==0.1.2 # homeassistant.components.device_tracker.bluetooth_tracker bt_proximity==0.1.2 +# homeassistant.components.device_tracker.bt_home_hub_5 +bthomehub5-devicelist==0.1.1 + # homeassistant.components.sensor.buienradar # homeassistant.components.weather.buienradar buienradar==0.91 diff --git a/tests/components/device_tracker/test_bt_home_hub_5.py b/tests/components/device_tracker/test_bt_home_hub_5.py deleted file mode 100644 index fd9692ec2b47c0..00000000000000 --- a/tests/components/device_tracker/test_bt_home_hub_5.py +++ /dev/null @@ -1,53 +0,0 @@ -"""The tests for the BT Home Hub 5 device tracker platform.""" -import unittest -from unittest.mock import patch - -from homeassistant.components.device_tracker import bt_home_hub_5 -from homeassistant.const import CONF_HOST - -patch_file = 'homeassistant.components.device_tracker.bt_home_hub_5' - - -def _get_homehub_data(url): - """Return mock homehub data.""" - return ''' - [ - { - "mac": "AA:BB:CC:DD:EE:FF, - "hostname": "hostname", - "ip": "192.168.1.43", - "ipv6": "", - "name": "hostname", - "activity": "1", - "os": "Unknown", - "device": "Unknown", - "time_first_seen": "2016/06/05 11:14:45", - "time_last_active": "2016/06/06 11:33:08", - "dhcp_option": "39043T90430T9TGK0EKGE5KGE3K904390K45GK054", - "port": "wl0", - "ipv6_ll": "fe80::gd67:ghrr:fuud:4332", - "activity_ip": "1", - "activity_ipv6_ll": "0", - "activity_ipv6": "0", - "device_oui": "NA", - "device_serial": "NA", - "device_class": "NA" - } - ] - ''' - - -class TestBTHomeHub5DeviceTracker(unittest.TestCase): - """Test BT Home Hub 5 device tracker platform.""" - - @patch('{}._get_homehub_data'.format(patch_file), new=_get_homehub_data) - def test_config_minimal(self): - """Test the setup with minimal configuration.""" - config = { - 'device_tracker': { - CONF_HOST: 'foo' - } - } - result = bt_home_hub_5.get_scanner(None, config) - - self.assertIsNotNone(result) From 12e69202f845ea92b57c0741dc2c01675a2f8c6e Mon Sep 17 00:00:00 2001 From: Jason Hu Date: Mon, 6 Aug 2018 01:25:37 -0700 Subject: [PATCH 051/117] Change to call_service async_stop non-blocking to allow service call finish (#15803) * Call later sync_stop to allow service call finish * Change to use non-blocking service all for restart and stop --- homeassistant/components/websocket_api.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/websocket_api.py b/homeassistant/components/websocket_api.py index ed478550c7a77c..d9c92fa357fe42 100644 --- a/homeassistant/components/websocket_api.py +++ b/homeassistant/components/websocket_api.py @@ -519,8 +519,12 @@ def handle_call_service(hass, connection, msg): """ async def call_service_helper(msg): """Call a service and fire complete message.""" + 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'), True, + msg['domain'], msg['service'], msg.get('service_data'), blocking, connection.context(msg)) connection.send_message_outside(result_message(msg['id'])) From 8ef2cfa364aa174f87c8f0f084446086ef0682f3 Mon Sep 17 00:00:00 2001 From: Jason Hu Date: Mon, 6 Aug 2018 01:51:37 -0700 Subject: [PATCH 052/117] Try to fix coveralls unstable result (#15800) * Create one tox env for code coverage report pytest-cov generated report in project root folder, not tox env folder. * Add cov tox env to travis * Coveralls seems expecting all build jobs upload * Only upload coverage after cov env success --- .travis.yml | 4 ++-- tox.ini | 18 +++++++++++++++++- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 0a3d710810cf69..920e8b57047740 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,7 +13,8 @@ matrix: - python: "3.5.3" env: TOXENV=typing - python: "3.5.3" - env: TOXENV=py35 + env: TOXENV=cov + after_success: coveralls - python: "3.6" env: TOXENV=py36 - python: "3.7" @@ -45,4 +46,3 @@ deploy: on: branch: dev condition: $TOXENV = lint -after_success: coveralls diff --git a/tox.ini b/tox.ini index fb36ac6511a8e4..d6ef1981bef000 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py35, py36, py37, py38, lint, pylint, typing +envlist = py35, py36, py37, py38, lint, pylint, typing, cov skip_missing_interpreters = True [testenv] @@ -11,6 +11,22 @@ setenv = ; fail. 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} +deps = + -r{toxinidir}/requirements_test_all.txt + -c{toxinidir}/homeassistant/package_constraints.txt + +[testenv:cov] +basepython = {env:PYTHON3_PATH:python3} +setenv = + PYTHONPATH = {toxinidir}:{toxinidir}/homeassistant +; both temper-python and XBee modules have utf8 in their README files +; which get read in from setup.py. If we don't force our locale to a +; utf8 one, tox's env is reset. And the install of these 2 packages +; fail. +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} deps = From 9cfe0db3c8476932a36905152341f8f291c9f3d2 Mon Sep 17 00:00:00 2001 From: John Arild Berentsen Date: Mon, 6 Aug 2018 11:10:26 +0200 Subject: [PATCH 053/117] Add different pop 012501 ID (#15838) --- homeassistant/components/zwave/discovery_schemas.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/zwave/discovery_schemas.py b/homeassistant/components/zwave/discovery_schemas.py index f88b911a6a5bf0..2a4e42ab92c21d 100644 --- a/homeassistant/components/zwave/discovery_schemas.py +++ b/homeassistant/components/zwave/discovery_schemas.py @@ -175,6 +175,7 @@ {const.DISC_COMPONENT: 'lock', const.DISC_GENERIC_DEVICE_CLASS: [const.GENERIC_TYPE_ENTRY_CONTROL], const.DISC_SPECIFIC_DEVICE_CLASS: [ + const.SPECIFIC_TYPE_DOOR_LOCK, const.SPECIFIC_TYPE_ADVANCED_DOOR_LOCK, const.SPECIFIC_TYPE_SECURE_KEYPAD_DOOR_LOCK, const.SPECIFIC_TYPE_SECURE_LOCKBOX], From 60318012061700cdc886f2ddc1b243e196463806 Mon Sep 17 00:00:00 2001 From: psike Date: Mon, 6 Aug 2018 13:18:36 +0300 Subject: [PATCH 054/117] Fix error when Series missing 'episodeFileCount' or 'episodeCount' (#15824) * Fix error when Series missing 'episodeFileCount' or 'episodeCount' * Update sonarr.py * Update sonarr.py --- homeassistant/components/sensor/sonarr.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/sonarr.py b/homeassistant/components/sensor/sonarr.py index 090addb5b6ee5a..c2fd6c8066330f 100644 --- a/homeassistant/components/sensor/sonarr.py +++ b/homeassistant/components/sensor/sonarr.py @@ -158,8 +158,12 @@ def device_state_attributes(self): ) elif self.type == 'series': for show in self.data: - attributes[show['title']] = '{}/{} Episodes'.format( - show['episodeFileCount'], show['episodeCount']) + if 'episodeFileCount' not in show \ + or 'episodeCount' not in show: + attributes[show['title']] = 'N/A' + else: + attributes[show['title']] = '{}/{} Episodes'.format( + show['episodeFileCount'], show['episodeCount']) elif self.type == 'status': attributes = self.data return attributes From 47fa92842575ef6d5b3d49351cb519977ee9feb7 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 6 Aug 2018 13:01:32 +0200 Subject: [PATCH 055/117] Bumped version to 0.76.0.dev0 --- homeassistant/const.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 6f0c5d50481eca..1b9dc8986a5902 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 = 75 -PATCH_VERSION = '2' +MINOR_VERSION = 76 +PATCH_VERSION = '0.dev0' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From 61721478f3715ad66d980c4b2682a1414204dea3 Mon Sep 17 00:00:00 2001 From: Robin Date: Tue, 7 Aug 2018 06:30:36 +0100 Subject: [PATCH 056/117] Add facebox auth (#15439) * Adds auth * Update facebox.py * Update test_facebox.py * Update facebox.py * Update facebox.py * Update facebox.py * Update facebox.py * Remove TIMEOUT * Update test_facebox.py * fix lint * Update facebox.py * Update test_facebox.py * Update facebox.py * Adds check_box_health * Adds test auth * Update test_facebox.py * Update test_facebox.py * Update test_facebox.py * Update test_facebox.py * Ups coverage * Update test_facebox.py * Update facebox.py * Update test_facebox.py * Update facebox.py * Update test_facebox.py * Update facebox.py * Update facebox.py * Update facebox.py --- .../components/image_processing/facebox.py | 137 ++++++++++------ .../image_processing/test_facebox.py | 153 ++++++++++++------ 2 files changed, 196 insertions(+), 94 deletions(-) diff --git a/homeassistant/components/image_processing/facebox.py b/homeassistant/components/image_processing/facebox.py index c863f8045138e4..e5ce0b825d0794 100644 --- a/homeassistant/components/image_processing/facebox.py +++ b/homeassistant/components/image_processing/facebox.py @@ -17,25 +17,29 @@ from homeassistant.components.image_processing import ( PLATFORM_SCHEMA, ImageProcessingFaceEntity, ATTR_CONFIDENCE, CONF_SOURCE, CONF_ENTITY_ID, CONF_NAME, DOMAIN) -from homeassistant.const import (CONF_IP_ADDRESS, CONF_PORT) +from homeassistant.const import ( + CONF_IP_ADDRESS, CONF_PORT, CONF_PASSWORD, CONF_USERNAME, + HTTP_BAD_REQUEST, HTTP_OK, HTTP_UNAUTHORIZED) _LOGGER = logging.getLogger(__name__) ATTR_BOUNDING_BOX = 'bounding_box' ATTR_CLASSIFIER = 'classifier' ATTR_IMAGE_ID = 'image_id' +ATTR_ID = 'id' ATTR_MATCHED = 'matched' +FACEBOX_NAME = 'name' CLASSIFIER = 'facebox' DATA_FACEBOX = 'facebox_classifiers' -EVENT_CLASSIFIER_TEACH = 'image_processing.teach_classifier' FILE_PATH = 'file_path' SERVICE_TEACH_FACE = 'facebox_teach_face' -TIMEOUT = 9 PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_IP_ADDRESS): cv.string, vol.Required(CONF_PORT): cv.port, + vol.Optional(CONF_USERNAME): cv.string, + vol.Optional(CONF_PASSWORD): cv.string, }) SERVICE_TEACH_SCHEMA = vol.Schema({ @@ -45,6 +49,26 @@ }) +def check_box_health(url, username, password): + """Check the health of the classifier and return its id if healthy.""" + kwargs = {} + if username: + kwargs['auth'] = requests.auth.HTTPBasicAuth(username, password) + try: + response = requests.get( + url, + **kwargs + ) + if response.status_code == HTTP_UNAUTHORIZED: + _LOGGER.error("AuthenticationError on %s", CLASSIFIER) + return None + if response.status_code == HTTP_OK: + return response.json()['hostname'] + except requests.exceptions.ConnectionError: + _LOGGER.error("ConnectionError: Is %s running?", CLASSIFIER) + return None + + def encode_image(image): """base64 encode an image stream.""" base64_img = base64.b64encode(image).decode('ascii') @@ -63,10 +87,10 @@ def parse_faces(api_faces): for entry in api_faces: face = {} if entry['matched']: # This data is only in matched faces. - face[ATTR_NAME] = entry['name'] + face[FACEBOX_NAME] = entry['name'] face[ATTR_IMAGE_ID] = entry['id'] else: # Lets be explicit. - face[ATTR_NAME] = None + face[FACEBOX_NAME] = None face[ATTR_IMAGE_ID] = None face[ATTR_CONFIDENCE] = round(100.0*entry['confidence'], 2) face[ATTR_MATCHED] = entry['matched'] @@ -75,17 +99,46 @@ def parse_faces(api_faces): return known_faces -def post_image(url, image): +def post_image(url, image, username, password): """Post an image to the classifier.""" + kwargs = {} + if username: + kwargs['auth'] = requests.auth.HTTPBasicAuth(username, password) try: response = requests.post( url, json={"base64": encode_image(image)}, - timeout=TIMEOUT + **kwargs ) + if response.status_code == HTTP_UNAUTHORIZED: + _LOGGER.error("AuthenticationError on %s", CLASSIFIER) + return None return response except requests.exceptions.ConnectionError: _LOGGER.error("ConnectionError: Is %s running?", CLASSIFIER) + return None + + +def teach_file(url, name, file_path, username, password): + """Teach the classifier a name associated with a file.""" + kwargs = {} + if username: + kwargs['auth'] = requests.auth.HTTPBasicAuth(username, password) + try: + with open(file_path, 'rb') as open_file: + response = requests.post( + url, + data={FACEBOX_NAME: name, ATTR_ID: file_path}, + files={'file': open_file}, + **kwargs + ) + if response.status_code == HTTP_UNAUTHORIZED: + _LOGGER.error("AuthenticationError on %s", CLASSIFIER) + elif response.status_code == HTTP_BAD_REQUEST: + _LOGGER.error("%s teaching of file %s failed with message:%s", + CLASSIFIER, file_path, response.text) + except requests.exceptions.ConnectionError: + _LOGGER.error("ConnectionError: Is %s running?", CLASSIFIER) def valid_file_path(file_path): @@ -104,13 +157,20 @@ def setup_platform(hass, config, add_devices, discovery_info=None): if DATA_FACEBOX not in hass.data: hass.data[DATA_FACEBOX] = [] + ip_address = config[CONF_IP_ADDRESS] + port = config[CONF_PORT] + username = config.get(CONF_USERNAME) + password = config.get(CONF_PASSWORD) + url_health = "http://{}:{}/healthz".format(ip_address, port) + hostname = check_box_health(url_health, username, password) + if hostname is None: + return + entities = [] for camera in config[CONF_SOURCE]: facebox = FaceClassifyEntity( - config[CONF_IP_ADDRESS], - config[CONF_PORT], - camera[CONF_ENTITY_ID], - camera.get(CONF_NAME)) + ip_address, port, username, password, hostname, + camera[CONF_ENTITY_ID], camera.get(CONF_NAME)) entities.append(facebox) hass.data[DATA_FACEBOX].append(facebox) add_devices(entities) @@ -129,33 +189,37 @@ def service_handle(service): classifier.teach(name, file_path) hass.services.register( - DOMAIN, - SERVICE_TEACH_FACE, - service_handle, + DOMAIN, SERVICE_TEACH_FACE, service_handle, schema=SERVICE_TEACH_SCHEMA) class FaceClassifyEntity(ImageProcessingFaceEntity): """Perform a face classification.""" - def __init__(self, ip, port, camera_entity, name=None): + def __init__(self, ip_address, port, username, password, hostname, + camera_entity, name=None): """Init with the API key and model id.""" super().__init__() - self._url_check = "http://{}:{}/{}/check".format(ip, port, CLASSIFIER) - self._url_teach = "http://{}:{}/{}/teach".format(ip, port, CLASSIFIER) + self._url_check = "http://{}:{}/{}/check".format( + ip_address, port, CLASSIFIER) + self._url_teach = "http://{}:{}/{}/teach".format( + ip_address, port, CLASSIFIER) + self._username = username + self._password = password + self._hostname = hostname self._camera = camera_entity if name: self._name = name else: camera_name = split_entity_id(camera_entity)[1] - self._name = "{} {}".format( - CLASSIFIER, camera_name) + self._name = "{} {}".format(CLASSIFIER, camera_name) self._matched = {} def process_image(self, image): """Process an image.""" - response = post_image(self._url_check, image) - if response is not None: + response = post_image( + self._url_check, image, self._username, self._password) + if response: response_json = response.json() if response_json['success']: total_faces = response_json['facesCount'] @@ -173,34 +237,8 @@ def teach(self, name, file_path): if (not self.hass.config.is_allowed_path(file_path) or not valid_file_path(file_path)): return - with open(file_path, 'rb') as open_file: - response = requests.post( - self._url_teach, - data={ATTR_NAME: name, 'id': file_path}, - files={'file': open_file}) - - if response.status_code == 200: - self.hass.bus.fire( - EVENT_CLASSIFIER_TEACH, { - ATTR_CLASSIFIER: CLASSIFIER, - ATTR_NAME: name, - FILE_PATH: file_path, - 'success': True, - 'message': None - }) - - elif response.status_code == 400: - _LOGGER.warning( - "%s teaching of file %s failed with message:%s", - CLASSIFIER, file_path, response.text) - self.hass.bus.fire( - EVENT_CLASSIFIER_TEACH, { - ATTR_CLASSIFIER: CLASSIFIER, - ATTR_NAME: name, - FILE_PATH: file_path, - 'success': False, - 'message': response.text - }) + teach_file( + self._url_teach, name, file_path, self._username, self._password) @property def camera_entity(self): @@ -218,4 +256,5 @@ def device_state_attributes(self): return { 'matched_faces': self._matched, 'total_matched_faces': len(self._matched), + 'hostname': self._hostname } diff --git a/tests/components/image_processing/test_facebox.py b/tests/components/image_processing/test_facebox.py index 86811f94db3fd8..b1d9fb8bf79d30 100644 --- a/tests/components/image_processing/test_facebox.py +++ b/tests/components/image_processing/test_facebox.py @@ -7,19 +7,19 @@ from homeassistant.core import callback from homeassistant.const import ( - ATTR_ENTITY_ID, ATTR_NAME, CONF_FRIENDLY_NAME, - CONF_IP_ADDRESS, CONF_PORT, STATE_UNKNOWN) + ATTR_ENTITY_ID, ATTR_NAME, CONF_FRIENDLY_NAME, CONF_PASSWORD, + CONF_USERNAME, CONF_IP_ADDRESS, CONF_PORT, + HTTP_BAD_REQUEST, HTTP_OK, HTTP_UNAUTHORIZED, STATE_UNKNOWN) from homeassistant.setup import async_setup_component import homeassistant.components.image_processing as ip import homeassistant.components.image_processing.facebox as fb -# pylint: disable=redefined-outer-name - MOCK_IP = '192.168.0.1' MOCK_PORT = '8080' # Mock data returned by the facebox API. -MOCK_ERROR = "No face found" +MOCK_BOX_ID = 'b893cc4f7fd6' +MOCK_ERROR_NO_FACE = "No face found" MOCK_FACE = {'confidence': 0.5812028911604818, 'id': 'john.jpg', 'matched': True, @@ -28,14 +28,21 @@ MOCK_FILE_PATH = '/images/mock.jpg' +MOCK_HEALTH = {'success': True, + 'hostname': 'b893cc4f7fd6', + 'metadata': {'boxname': 'facebox', 'build': 'development'}, + 'errors': []} + MOCK_JSON = {"facesCount": 1, "success": True, "faces": [MOCK_FACE]} MOCK_NAME = 'mock_name' +MOCK_USERNAME = 'mock_username' +MOCK_PASSWORD = 'mock_password' # Faces data after parsing. -PARSED_FACES = [{ATTR_NAME: 'John Lennon', +PARSED_FACES = [{fb.FACEBOX_NAME: 'John Lennon', fb.ATTR_IMAGE_ID: 'john.jpg', fb.ATTR_CONFIDENCE: 58.12, fb.ATTR_MATCHED: True, @@ -62,6 +69,15 @@ } +@pytest.fixture +def mock_healthybox(): + """Mock fb.check_box_health.""" + check_box_health = 'homeassistant.components.image_processing.' \ + 'facebox.check_box_health' + with patch(check_box_health, return_value=MOCK_BOX_ID) as _mock_healthybox: + yield _mock_healthybox + + @pytest.fixture def mock_isfile(): """Mock os.path.isfile.""" @@ -70,6 +86,14 @@ def mock_isfile(): yield _mock_isfile +@pytest.fixture +def mock_image(): + """Return a mock camera image.""" + with patch('homeassistant.components.camera.demo.DemoCamera.camera_image', + return_value=b'Test') as image: + yield image + + @pytest.fixture def mock_open_file(): """Mock open.""" @@ -79,6 +103,22 @@ def mock_open_file(): yield _mock_open +def test_check_box_health(caplog): + """Test check box health.""" + with requests_mock.Mocker() as mock_req: + url = "http://{}:{}/healthz".format(MOCK_IP, MOCK_PORT) + mock_req.get(url, status_code=HTTP_OK, json=MOCK_HEALTH) + assert fb.check_box_health(url, 'user', 'pass') == MOCK_BOX_ID + + mock_req.get(url, status_code=HTTP_UNAUTHORIZED) + assert fb.check_box_health(url, None, None) is None + assert "AuthenticationError on facebox" in caplog.text + + mock_req.get(url, exc=requests.exceptions.ConnectTimeout) + fb.check_box_health(url, None, None) + assert "ConnectionError: Is facebox running?" in caplog.text + + def test_encode_image(): """Test that binary data is encoded correctly.""" assert fb.encode_image(b'test') == 'dGVzdA==' @@ -100,22 +140,24 @@ def test_valid_file_path(): assert not fb.valid_file_path('test_path') -@pytest.fixture -def mock_image(): - """Return a mock camera image.""" - with patch('homeassistant.components.camera.demo.DemoCamera.camera_image', - return_value=b'Test') as image: - yield image - - -async def test_setup_platform(hass): +async def test_setup_platform(hass, mock_healthybox): """Setup platform with one entity.""" await async_setup_component(hass, ip.DOMAIN, VALID_CONFIG) assert hass.states.get(VALID_ENTITY_ID) -async def test_process_image(hass, mock_image): - """Test processing of an image.""" +async def test_setup_platform_with_auth(hass, mock_healthybox): + """Setup platform with one entity and auth.""" + valid_config_auth = VALID_CONFIG.copy() + valid_config_auth[ip.DOMAIN][CONF_USERNAME] = MOCK_USERNAME + valid_config_auth[ip.DOMAIN][CONF_PASSWORD] = MOCK_PASSWORD + + await async_setup_component(hass, ip.DOMAIN, valid_config_auth) + assert hass.states.get(VALID_ENTITY_ID) + + +async def test_process_image(hass, mock_healthybox, mock_image): + """Test successful processing of an image.""" await async_setup_component(hass, ip.DOMAIN, VALID_CONFIG) assert hass.states.get(VALID_ENTITY_ID) @@ -157,11 +199,12 @@ def mock_face_event(event): PARSED_FACES[0][fb.ATTR_BOUNDING_BOX]) -async def test_connection_error(hass, mock_image): - """Test connection error.""" +async def test_process_image_errors(hass, mock_healthybox, mock_image, caplog): + """Test process_image errors.""" await async_setup_component(hass, ip.DOMAIN, VALID_CONFIG) assert hass.states.get(VALID_ENTITY_ID) + # Test connection error. with requests_mock.Mocker() as mock_req: url = "http://{}:{}/facebox/check".format(MOCK_IP, MOCK_PORT) mock_req.register_uri( @@ -171,34 +214,40 @@ async def test_connection_error(hass, mock_image): ip.SERVICE_SCAN, service_data=data) await hass.async_block_till_done() + assert "ConnectionError: Is facebox running?" in caplog.text state = hass.states.get(VALID_ENTITY_ID) assert state.state == STATE_UNKNOWN assert state.attributes.get('faces') == [] assert state.attributes.get('matched_faces') == {} + # Now test with bad auth. + with requests_mock.Mocker() as mock_req: + url = "http://{}:{}/facebox/check".format(MOCK_IP, MOCK_PORT) + mock_req.register_uri( + 'POST', url, status_code=HTTP_UNAUTHORIZED) + data = {ATTR_ENTITY_ID: VALID_ENTITY_ID} + await hass.services.async_call(ip.DOMAIN, + ip.SERVICE_SCAN, + service_data=data) + await hass.async_block_till_done() + assert "AuthenticationError on facebox" in caplog.text + -async def test_teach_service(hass, mock_image, mock_isfile, mock_open_file): +async def test_teach_service( + hass, mock_healthybox, mock_image, + mock_isfile, mock_open_file, caplog): """Test teaching of facebox.""" await async_setup_component(hass, ip.DOMAIN, VALID_CONFIG) assert hass.states.get(VALID_ENTITY_ID) - teach_events = [] - - @callback - def mock_teach_event(event): - """Mock event.""" - teach_events.append(event) - - hass.bus.async_listen( - 'image_processing.teach_classifier', mock_teach_event) - # Patch out 'is_allowed_path' as the mock files aren't allowed hass.config.is_allowed_path = Mock(return_value=True) + # Test successful teach. with requests_mock.Mocker() as mock_req: url = "http://{}:{}/facebox/teach".format(MOCK_IP, MOCK_PORT) - mock_req.post(url, status_code=200) + mock_req.post(url, status_code=HTTP_OK) data = {ATTR_ENTITY_ID: VALID_ENTITY_ID, ATTR_NAME: MOCK_NAME, fb.FILE_PATH: MOCK_FILE_PATH} @@ -206,17 +255,24 @@ def mock_teach_event(event): ip.DOMAIN, fb.SERVICE_TEACH_FACE, service_data=data) await hass.async_block_till_done() - assert len(teach_events) == 1 - assert teach_events[0].data[fb.ATTR_CLASSIFIER] == fb.CLASSIFIER - assert teach_events[0].data[ATTR_NAME] == MOCK_NAME - assert teach_events[0].data[fb.FILE_PATH] == MOCK_FILE_PATH - assert teach_events[0].data['success'] - assert not teach_events[0].data['message'] + # Now test with bad auth. + with requests_mock.Mocker() as mock_req: + url = "http://{}:{}/facebox/teach".format(MOCK_IP, MOCK_PORT) + mock_req.post(url, status_code=HTTP_UNAUTHORIZED) + data = {ATTR_ENTITY_ID: VALID_ENTITY_ID, + ATTR_NAME: MOCK_NAME, + fb.FILE_PATH: MOCK_FILE_PATH} + await hass.services.async_call(ip.DOMAIN, + fb.SERVICE_TEACH_FACE, + service_data=data) + await hass.async_block_till_done() + assert "AuthenticationError on facebox" in caplog.text # Now test the failed teaching. with requests_mock.Mocker() as mock_req: url = "http://{}:{}/facebox/teach".format(MOCK_IP, MOCK_PORT) - mock_req.post(url, status_code=400, text=MOCK_ERROR) + mock_req.post(url, status_code=HTTP_BAD_REQUEST, + text=MOCK_ERROR_NO_FACE) data = {ATTR_ENTITY_ID: VALID_ENTITY_ID, ATTR_NAME: MOCK_NAME, fb.FILE_PATH: MOCK_FILE_PATH} @@ -224,16 +280,23 @@ def mock_teach_event(event): fb.SERVICE_TEACH_FACE, service_data=data) await hass.async_block_till_done() + assert MOCK_ERROR_NO_FACE in caplog.text - assert len(teach_events) == 2 - assert teach_events[1].data[fb.ATTR_CLASSIFIER] == fb.CLASSIFIER - assert teach_events[1].data[ATTR_NAME] == MOCK_NAME - assert teach_events[1].data[fb.FILE_PATH] == MOCK_FILE_PATH - assert not teach_events[1].data['success'] - assert teach_events[1].data['message'] == MOCK_ERROR + # Now test connection error. + with requests_mock.Mocker() as mock_req: + url = "http://{}:{}/facebox/teach".format(MOCK_IP, MOCK_PORT) + mock_req.post(url, exc=requests.exceptions.ConnectTimeout) + data = {ATTR_ENTITY_ID: VALID_ENTITY_ID, + ATTR_NAME: MOCK_NAME, + fb.FILE_PATH: MOCK_FILE_PATH} + await hass.services.async_call(ip.DOMAIN, + fb.SERVICE_TEACH_FACE, + service_data=data) + await hass.async_block_till_done() + assert "ConnectionError: Is facebox running?" in caplog.text -async def test_setup_platform_with_name(hass): +async def test_setup_platform_with_name(hass, mock_healthybox): """Setup platform with one entity and a name.""" named_entity_id = 'image_processing.{}'.format(MOCK_NAME) From a7db2ebbe1bf9770614719e55a6a7703fc7ca9d1 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Tue, 7 Aug 2018 09:01:32 +0200 Subject: [PATCH 057/117] Upgrade requests_mock to 1.5.2 --- 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 12c5abff404cda..1e71494659e88f 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -14,4 +14,4 @@ pytest-cov==2.5.1 pytest-sugar==0.9.1 pytest-timeout==1.3.1 pytest==3.7.1 -requests_mock==1.5 +requests_mock==1.5.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 83787946320b2b..e43eed485b424d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -15,7 +15,7 @@ pytest-cov==2.5.1 pytest-sugar==0.9.1 pytest-timeout==1.3.1 pytest==3.7.1 -requests_mock==1.5 +requests_mock==1.5.2 # homeassistant.components.homekit From cb20c9b1ea1ff2d40480a87f3e1dee372808ba40 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Tue, 7 Aug 2018 09:02:54 +0200 Subject: [PATCH 058/117] Revert "Upgrade requests_mock to 1.5.2" This reverts commit a7db2ebbe1bf9770614719e55a6a7703fc7ca9d1. --- 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 1e71494659e88f..12c5abff404cda 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -14,4 +14,4 @@ pytest-cov==2.5.1 pytest-sugar==0.9.1 pytest-timeout==1.3.1 pytest==3.7.1 -requests_mock==1.5.2 +requests_mock==1.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e43eed485b424d..83787946320b2b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -15,7 +15,7 @@ pytest-cov==2.5.1 pytest-sugar==0.9.1 pytest-timeout==1.3.1 pytest==3.7.1 -requests_mock==1.5.2 +requests_mock==1.5 # homeassistant.components.homekit From 51c30980df50d8bb40784c31320163e00c258b7a Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Tue, 7 Aug 2018 09:12:09 +0200 Subject: [PATCH 059/117] Upgrade holidays to 0.9.6 (#15831) --- homeassistant/components/binary_sensor/workday.py | 8 ++++---- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/binary_sensor/workday.py b/homeassistant/components/binary_sensor/workday.py index 00d2a95e356b40..4a9809e9974f1f 100644 --- a/homeassistant/components/binary_sensor/workday.py +++ b/homeassistant/components/binary_sensor/workday.py @@ -17,7 +17,7 @@ _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['holidays==0.9.5'] +REQUIREMENTS = ['holidays==0.9.6'] # List of all countries currently supported by holidays # There seems to be no way to get the list out at runtime @@ -25,9 +25,9 @@ 'Belgium', 'BE', 'Canada', 'CA', 'Colombia', 'CO', 'Czech', 'CZ', 'Denmark', 'DK', 'England', 'EuropeanCentralBank', 'ECB', 'TAR', 'Finland', 'FI', 'France', 'FRA', 'Germany', - 'DE', 'Hungary', 'HU', 'Ireland', 'Isle of Man', 'Italy', - 'IT', 'Japan', 'JP', 'Mexico', 'MX', 'Netherlands', 'NL', - 'NewZealand', 'NZ', 'Northern Ireland', + 'DE', 'Hungary', 'HU', 'India', 'IND', 'Ireland', + 'Isle of Man', 'Italy', 'IT', 'Japan', 'JP', 'Mexico', 'MX', + 'Netherlands', 'NL', 'NewZealand', 'NZ', 'Northern Ireland', 'Norway', 'NO', 'Polish', 'PL', 'Portugal', 'PT', 'PortugalExt', 'PTE', 'Scotland', 'Slovenia', 'SI', 'Slovakia', 'SK', 'South Africa', 'ZA', 'Spain', 'ES', diff --git a/requirements_all.txt b/requirements_all.txt index 2ee58f9033471c..d41a8ba110b16a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -427,7 +427,7 @@ hipnotify==1.0.8 hole==0.3.0 # homeassistant.components.binary_sensor.workday -holidays==0.9.5 +holidays==0.9.6 # homeassistant.components.frontend home-assistant-frontend==20180804.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 83787946320b2b..de85e09c3ff0e2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -78,7 +78,7 @@ haversine==0.4.5 hbmqtt==0.9.2 # homeassistant.components.binary_sensor.workday -holidays==0.9.5 +holidays==0.9.6 # homeassistant.components.frontend home-assistant-frontend==20180804.0 From 1d8678c431e451916b6a701dccb182ba6b7e9858 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Tue, 7 Aug 2018 09:13:01 +0200 Subject: [PATCH 060/117] Upgrade pysnmp to 4.4.5 (#15854) --- homeassistant/components/device_tracker/snmp.py | 2 +- homeassistant/components/sensor/snmp.py | 2 +- homeassistant/components/switch/snmp.py | 2 +- requirements_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/device_tracker/snmp.py b/homeassistant/components/device_tracker/snmp.py index 6a849d0b05abfd..a9afc76e67c273 100644 --- a/homeassistant/components/device_tracker/snmp.py +++ b/homeassistant/components/device_tracker/snmp.py @@ -14,7 +14,7 @@ DOMAIN, PLATFORM_SCHEMA, DeviceScanner) from homeassistant.const import CONF_HOST -REQUIREMENTS = ['pysnmp==4.4.4'] +REQUIREMENTS = ['pysnmp==4.4.5'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sensor/snmp.py b/homeassistant/components/sensor/snmp.py index 5600f906f342ca..e6119ab80b6d31 100644 --- a/homeassistant/components/sensor/snmp.py +++ b/homeassistant/components/sensor/snmp.py @@ -16,7 +16,7 @@ CONF_HOST, CONF_NAME, CONF_PORT, CONF_UNIT_OF_MEASUREMENT, STATE_UNKNOWN, CONF_VALUE_TEMPLATE) -REQUIREMENTS = ['pysnmp==4.4.4'] +REQUIREMENTS = ['pysnmp==4.4.5'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/switch/snmp.py b/homeassistant/components/switch/snmp.py index b0c192cdafad42..9c84584e833b9e 100644 --- a/homeassistant/components/switch/snmp.py +++ b/homeassistant/components/switch/snmp.py @@ -13,7 +13,7 @@ CONF_HOST, CONF_NAME, CONF_PORT, CONF_PAYLOAD_ON, CONF_PAYLOAD_OFF) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['pysnmp==4.4.4'] +REQUIREMENTS = ['pysnmp==4.4.5'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index d41a8ba110b16a..5b5d4fe88ad9d5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1017,7 +1017,7 @@ pysma==0.2 # homeassistant.components.device_tracker.snmp # homeassistant.components.sensor.snmp # homeassistant.components.switch.snmp -pysnmp==4.4.4 +pysnmp==4.4.5 # homeassistant.components.notify.stride pystride==0.1.7 From f09f153014fcefa911af3586cb45068274ecde79 Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Tue, 7 Aug 2018 09:26:58 +0200 Subject: [PATCH 061/117] Fix HomeKit test (#15860) * Don't raise NotImplementedError during test --- tests/components/homekit/test_accessories.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/components/homekit/test_accessories.py b/tests/components/homekit/test_accessories.py index 23706f02e75499..edb1c7175f85fc 100644 --- a/tests/components/homekit/test_accessories.py +++ b/tests/components/homekit/test_accessories.py @@ -104,6 +104,7 @@ async def test_battery_service(hass, hk_driver): await hass.async_block_till_done() acc = HomeAccessory(hass, hk_driver, 'Battery Service', entity_id, 2, None) + acc.update_state = lambda x: None assert acc._char_battery.value == 0 assert acc._char_low_battery.value == 0 assert acc._char_charging.value == 2 @@ -126,6 +127,7 @@ async def test_battery_service(hass, hk_driver): await hass.async_block_till_done() acc = HomeAccessory(hass, hk_driver, 'Battery Service', entity_id, 2, None) + acc.update_state = lambda x: None assert acc._char_battery.value == 0 assert acc._char_low_battery.value == 0 assert acc._char_charging.value == 2 From d071df0decc047c0b12b33d2d92625e5cab65c49 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 7 Aug 2018 09:27:40 +0200 Subject: [PATCH 062/117] Do not make internet connection during tests (#15858) * Do not make internet connection * Small improvement --- tests/components/cloud/test_init.py | 7 ++++--- tests/components/homekit/conftest.py | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/components/cloud/test_init.py b/tests/components/cloud/test_init.py index 91f8ab8316ded0..014cdb1c6c6f18 100644 --- a/tests/components/cloud/test_init.py +++ b/tests/components/cloud/test_init.py @@ -73,8 +73,7 @@ def test_constructor_loads_info_from_config(): assert cl.relayer == 'test-relayer' -@asyncio.coroutine -def test_initialize_loads_info(mock_os, hass): +async def test_initialize_loads_info(mock_os, hass): """Test initialize will load info from config file.""" mock_os.path.isfile.return_value = True mopen = mock_open(read_data=json.dumps({ @@ -88,8 +87,10 @@ def test_initialize_loads_info(mock_os, hass): cl.iot.connect.return_value = mock_coro() with patch('homeassistant.components.cloud.open', mopen, create=True), \ + patch('homeassistant.components.cloud.Cloud._fetch_jwt_keyset', + return_value=mock_coro(True)), \ patch('homeassistant.components.cloud.Cloud._decode_claims'): - yield from cl.async_start(None) + await cl.async_start(None) assert cl.id_token == 'test-id-token' assert cl.access_token == 'test-access-token' diff --git a/tests/components/homekit/conftest.py b/tests/components/homekit/conftest.py index f783926593950f..55e02de752608c 100644 --- a/tests/components/homekit/conftest.py +++ b/tests/components/homekit/conftest.py @@ -13,4 +13,4 @@ def hk_driver(): patch('pyhap.accessory_driver.AccessoryEncoder'), \ patch('pyhap.accessory_driver.HAPServer'), \ patch('pyhap.accessory_driver.AccessoryDriver.publish'): - return AccessoryDriver(pincode=b'123-45-678') + return AccessoryDriver(pincode=b'123-45-678', address='127.0.0.1') From 4cbcb4c3a27bb1cc9eac85fc47c27f63739f1823 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Tue, 7 Aug 2018 17:09:19 +0300 Subject: [PATCH 063/117] Upgrade pylint to 2.1.1 (#15872) --- 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 12c5abff404cda..3e89454ba27398 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -8,7 +8,7 @@ flake8==3.5 mock-open==1.3.1 mypy==0.620 pydocstyle==1.1.1 -pylint==2.1.0 +pylint==2.1.1 pytest-aiohttp==0.3.0 pytest-cov==2.5.1 pytest-sugar==0.9.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index de85e09c3ff0e2..1b230e5759f97f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -9,7 +9,7 @@ flake8==3.5 mock-open==1.3.1 mypy==0.620 pydocstyle==1.1.1 -pylint==2.1.0 +pylint==2.1.1 pytest-aiohttp==0.3.0 pytest-cov==2.5.1 pytest-sugar==0.9.1 From 1fb2ea70c2c2f06d87ba4f3c67280f2033a50326 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Tue, 7 Aug 2018 16:11:47 +0200 Subject: [PATCH 064/117] Upgrade asynctest to 0.12.2 (#15869) --- 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 3e89454ba27398..c2f78af7608789 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,7 +1,7 @@ # linters such as flake8 and pylint should be pinned, as new releases # make new things fail. Manually update these pins when pulling in a # new version -asynctest==0.12.1 +asynctest==0.12.2 coveralls==1.2.0 flake8-docstrings==1.0.3 flake8==3.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1b230e5759f97f..b2d2d662c8c110 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2,7 +2,7 @@ # linters such as flake8 and pylint should be pinned, as new releases # make new things fail. Manually update these pins when pulling in a # new version -asynctest==0.12.1 +asynctest==0.12.2 coveralls==1.2.0 flake8-docstrings==1.0.3 flake8==3.5 From d556edae3188ed045057a68706a8b95292b78beb Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Tue, 7 Aug 2018 16:12:01 +0200 Subject: [PATCH 065/117] Upgrade Sphinx to 1.7.6 (#15868) --- requirements_docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_docs.txt b/requirements_docs.txt index 0556b35fc08f62..a7436cad2fcf16 100644 --- a/requirements_docs.txt +++ b/requirements_docs.txt @@ -1,3 +1,3 @@ -Sphinx==1.7.5 +Sphinx==1.7.6 sphinx-autodoc-typehints==1.3.0 sphinx-autodoc-annotation==1.0.post1 From b6bc0097b821077a5ddd2abef3fbb4ddcc3f9042 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Tue, 7 Aug 2018 16:12:16 +0200 Subject: [PATCH 066/117] Upgrade requests_mock to 1.5.2 (#15867) --- 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 c2f78af7608789..5c2bd3404ed3b4 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -14,4 +14,4 @@ pytest-cov==2.5.1 pytest-sugar==0.9.1 pytest-timeout==1.3.1 pytest==3.7.1 -requests_mock==1.5 +requests_mock==1.5.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b2d2d662c8c110..a4b43f4ccc7751 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -15,7 +15,7 @@ pytest-cov==2.5.1 pytest-sugar==0.9.1 pytest-timeout==1.3.1 pytest==3.7.1 -requests_mock==1.5 +requests_mock==1.5.2 # homeassistant.components.homekit From fcc918a146620ba6b641ddde1e8dd846b53121c4 Mon Sep 17 00:00:00 2001 From: DubhAd Date: Tue, 7 Aug 2018 17:12:36 +0100 Subject: [PATCH 067/117] Update based upon forum post (#15876) Based upon [this post](https://community.home-assistant.io/t/device-tracker-ping-on-windows-not-working-solved/61474/3) it looks like we've found why people couldn't get the ping tracker working on Windows. --- homeassistant/components/device_tracker/ping.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/device_tracker/ping.py b/homeassistant/components/device_tracker/ping.py index d09e1930d4f7ee..f3492da9e80b59 100644 --- a/homeassistant/components/device_tracker/ping.py +++ b/homeassistant/components/device_tracker/ping.py @@ -38,7 +38,7 @@ def __init__(self, ip_address, dev_id, hass, config): self.dev_id = dev_id self._count = config[CONF_PING_COUNT] if sys.platform == 'win32': - self._ping_cmd = ['ping', '-n 1', '-w', '1000', self.ip_address] + self._ping_cmd = ['ping', '-n', '1', '-w', '1000', self.ip_address] else: self._ping_cmd = ['ping', '-n', '-q', '-c1', '-W1', self.ip_address] From debdc707e92cc4918e372a6f5d906c4568433b67 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Wed, 8 Aug 2018 11:53:43 +0200 Subject: [PATCH 068/117] Upgrade netdisco to 2.0.0 (#15885) --- homeassistant/components/discovery.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/discovery.py b/homeassistant/components/discovery.py index 8877f05f622587..e84eaef51b0404 100644 --- a/homeassistant/components/discovery.py +++ b/homeassistant/components/discovery.py @@ -21,7 +21,7 @@ from homeassistant.helpers.discovery import async_load_platform, async_discover import homeassistant.util.dt as dt_util -REQUIREMENTS = ['netdisco==1.5.0'] +REQUIREMENTS = ['netdisco==2.0.0'] DOMAIN = 'discovery' diff --git a/requirements_all.txt b/requirements_all.txt index 5b5d4fe88ad9d5..250f0bc1147181 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -595,7 +595,7 @@ ndms2_client==0.0.3 netdata==0.1.2 # homeassistant.components.discovery -netdisco==1.5.0 +netdisco==2.0.0 # homeassistant.components.sensor.neurio_energy neurio==0.3.1 From 0ab65f1ac54b9a708455ea4c50ffcdfee1e86fba Mon Sep 17 00:00:00 2001 From: Steven Looman Date: Wed, 8 Aug 2018 11:54:22 +0200 Subject: [PATCH 069/117] Follow changes to netdisco, separating DLNA into DLNA_DMS and DLNA_DMR (#15877) * Follow changes to netdisco, separating DLNA into DLNA_DMS and DLNA_DMR * No uppercase for names of netdisco discoverables --- homeassistant/components/discovery.py | 2 +- homeassistant/components/media_player/dlna_dmr.py | 14 -------------- 2 files changed, 1 insertion(+), 15 deletions(-) diff --git a/homeassistant/components/discovery.py b/homeassistant/components/discovery.py index e84eaef51b0404..8272fa9814aea1 100644 --- a/homeassistant/components/discovery.py +++ b/homeassistant/components/discovery.py @@ -85,7 +85,7 @@ 'volumio': ('media_player', 'volumio'), 'nanoleaf_aurora': ('light', 'nanoleaf_aurora'), 'freebox': ('device_tracker', 'freebox'), - 'DLNA': ('media_player', 'dlna_dmr') + 'dlna_dmr': ('media_player', 'dlna_dmr'), } OPTIONAL_SERVICE_HANDLERS = { diff --git a/homeassistant/components/media_player/dlna_dmr.py b/homeassistant/components/media_player/dlna_dmr.py index 98cd865b703f2d..9b6beb833412f8 100644 --- a/homeassistant/components/media_player/dlna_dmr.py +++ b/homeassistant/components/media_player/dlna_dmr.py @@ -67,11 +67,6 @@ 'channel': 'video/*', 'playlist': 'playlist/*', } -UPNP_DEVICE_MEDIA_RENDERER = [ - 'urn:schemas-upnp-org:device:MediaRenderer:1', - 'urn:schemas-upnp-org:device:MediaRenderer:2', - 'urn:schemas-upnp-org:device:MediaRenderer:3', -] _LOGGER = logging.getLogger(__name__) @@ -126,15 +121,6 @@ async def async_setup_platform(hass: HomeAssistant, async_add_devices, discovery_info=None): """Set up DLNA DMR platform.""" - # ensure this is a DLNA DMR device, if found via discovery - if discovery_info and \ - 'upnp_device_type' in discovery_info and \ - discovery_info['upnp_device_type'] not in UPNP_DEVICE_MEDIA_RENDERER: - _LOGGER.debug('Device is not a MediaRenderer: %s, device_type: %s', - discovery_info.get('ssdp_description'), - discovery_info['upnp_device_type']) - return - if config.get(CONF_URL) is not None: url = config[CONF_URL] name = config.get(CONF_NAME) From 61901496ec401ab50ca869ee25a25648d3876df0 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Wed, 8 Aug 2018 22:32:21 +0200 Subject: [PATCH 070/117] Upgrade pylast to 2.4.0 (#15886) --- homeassistant/components/sensor/lastfm.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/lastfm.py b/homeassistant/components/sensor/lastfm.py index 6ee3f7d16d0835..45eddee9f7e174 100644 --- a/homeassistant/components/sensor/lastfm.py +++ b/homeassistant/components/sensor/lastfm.py @@ -13,7 +13,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['pylast==2.3.0'] +REQUIREMENTS = ['pylast==2.4.0'] ATTR_LAST_PLAYED = 'last_played' ATTR_PLAY_COUNT = 'play_count' diff --git a/requirements_all.txt b/requirements_all.txt index 250f0bc1147181..23f90b19010e83 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -904,7 +904,7 @@ pykwb==0.0.8 pylacrosse==0.3.1 # homeassistant.components.sensor.lastfm -pylast==2.3.0 +pylast==2.4.0 # homeassistant.components.media_player.lg_netcast pylgnetcast-homeassistant==0.2.0.dev0 From 99c4c65f6926ace28a69f72f9c7759339fda7b29 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 9 Aug 2018 09:27:54 +0200 Subject: [PATCH 071/117] Add auth/authorize endpoint (#15887) --- homeassistant/components/frontend/__init__.py | 30 +++++++++++++++++++ tests/components/frontend/test_init.py | 11 +++++++ 2 files changed, 41 insertions(+) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 540341c68f2884..0dcf7526262d5d 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -249,6 +249,7 @@ async def async_setup(hass, config): index_view = IndexView(repo_path, js_version, hass.auth.active) hass.http.register_view(index_view) + hass.http.register_view(AuthorizeView(repo_path, js_version)) @callback def async_finalize_panel(panel): @@ -334,6 +335,35 @@ def reload_themes(_): hass.services.async_register(DOMAIN, SERVICE_RELOAD_THEMES, reload_themes) +class AuthorizeView(HomeAssistantView): + """Serve the frontend.""" + + url = '/auth/authorize' + name = 'auth:authorize' + requires_auth = False + + def __init__(self, repo_path, js_option): + """Initialize the frontend view.""" + self.repo_path = repo_path + self.js_option = js_option + + async def get(self, request: web.Request): + """Redirect to the authorize page.""" + latest = self.repo_path is not None or \ + _is_latest(self.js_option, request) + + if latest: + location = '/frontend_latest/authorize.html' + else: + location = '/frontend_es5/authorize.html' + + location += '?{}'.format(request.query_string) + + return web.Response(status=302, headers={ + 'location': location + }) + + class IndexView(HomeAssistantView): """Serve the frontend.""" diff --git a/tests/components/frontend/test_init.py b/tests/components/frontend/test_init.py index dfa67f48614c13..17bf3d953ef94f 100644 --- a/tests/components/frontend/test_init.py +++ b/tests/components/frontend/test_init.py @@ -348,3 +348,14 @@ async def test_onboarding_load(mock_http_client): """Test onboarding component loaded by default.""" resp = await mock_http_client.get('/api/onboarding') assert resp.status == 200 + + +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?latest&hello=world') + assert resp.url.query_string == 'latest&hello=world' + assert resp.url.path == '/frontend_latest/authorize.html' From 39d19f2183f00b7e485185ed2ea8cdf790724acb Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Thu, 9 Aug 2018 13:05:28 +0200 Subject: [PATCH 072/117] Upgrade locationsharinglib to 2.0.11 (#15902) --- .../components/device_tracker/google_maps.py | 11 ++++++----- requirements_all.txt | 2 +- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/device_tracker/google_maps.py b/homeassistant/components/device_tracker/google_maps.py index 1aeeb0e1030886..8c21e71bd3097a 100644 --- a/homeassistant/components/device_tracker/google_maps.py +++ b/homeassistant/components/device_tracker/google_maps.py @@ -17,29 +17,30 @@ from homeassistant.helpers.typing import ConfigType from homeassistant.util import slugify -REQUIREMENTS = ['locationsharinglib==2.0.7'] +REQUIREMENTS = ['locationsharinglib==2.0.11'] _LOGGER = logging.getLogger(__name__) -CONF_MAX_GPS_ACCURACY = 'max_gps_accuracy' ATTR_ADDRESS = 'address' ATTR_FULL_NAME = 'full_name' ATTR_LAST_SEEN = 'last_seen' ATTR_NICKNAME = 'nickname' +CONF_MAX_GPS_ACCURACY = 'max_gps_accuracy' + CREDENTIALS_FILE = '.google_maps_location_sharing.cookies' MIN_TIME_BETWEEN_SCANS = timedelta(seconds=30) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_MAX_GPS_ACCURACY, default=100000): vol.Coerce(float), - vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, + vol.Required(CONF_USERNAME): cv.string, + vol.Optional(CONF_MAX_GPS_ACCURACY, default=100000): vol.Coerce(float), }) def setup_scanner(hass, config: ConfigType, see, discovery_info=None): - """Set up the scanner.""" + """Set up the Google Maps Location sharing scanner.""" scanner = GoogleMapsScanner(hass, config, see) return scanner.success_init diff --git a/requirements_all.txt b/requirements_all.txt index 23f90b19010e83..6ec99e849536ef 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -533,7 +533,7 @@ liveboxplaytv==2.0.2 lmnotify==0.0.4 # homeassistant.components.device_tracker.google_maps -locationsharinglib==2.0.7 +locationsharinglib==2.0.11 # homeassistant.components.sensor.luftdaten luftdaten==0.2.0 From f58425dd3cc4f8f3c492102aa448c7b93bab7128 Mon Sep 17 00:00:00 2001 From: Jason Hu Date: Thu, 9 Aug 2018 04:24:14 -0700 Subject: [PATCH 073/117] Refactor data entry flow (#15883) * Refactoring data_entry_flow and config_entry_flow Move SOURCE_* to config_entries Change data_entry_flow.FlowManager.async_init() source param default to None Change this first step_id as source or init if source is None _BaseFlowManagerView pass in SOURCE_USER as default source * First step of data entry flow decided by _async_create_flow() now * Lint * Change helpers.config_entry_flow.DiscoveryFlowHandler default step * Change FlowManager.async_init source param to context dict param --- homeassistant/auth/__init__.py | 2 +- homeassistant/components/cast/__init__.py | 4 +- .../components/config/config_entries.py | 2 +- homeassistant/components/deconz/__init__.py | 5 +- .../components/deconz/config_flow.py | 4 ++ homeassistant/components/discovery.py | 4 +- .../components/homematicip_cloud/__init__.py | 4 +- .../homematicip_cloud/config_flow.py | 4 ++ homeassistant/components/hue/__init__.py | 5 +- homeassistant/components/hue/bridge.py | 3 +- homeassistant/components/hue/config_flow.py | 4 ++ homeassistant/components/nest/__init__.py | 4 +- homeassistant/components/nest/config_flow.py | 4 ++ homeassistant/components/sonos/__init__.py | 4 +- homeassistant/components/zone/config_flow.py | 4 ++ homeassistant/config_entries.py | 48 +++++++++++++------ homeassistant/data_entry_flow.py | 24 ++++------ homeassistant/helpers/config_entry_flow.py | 2 +- homeassistant/helpers/data_entry_flow.py | 5 +- tests/common.py | 4 +- .../components/config/test_config_entries.py | 18 +++---- tests/components/test_discovery.py | 4 +- tests/helpers/test_config_entry_flow.py | 12 ++--- tests/test_config_entries.py | 10 ++-- tests/test_data_entry_flow.py | 23 +++++---- 25 files changed, 128 insertions(+), 79 deletions(-) diff --git a/homeassistant/auth/__init__.py b/homeassistant/auth/__init__.py index 35804cd8483cdc..8eaa9cdbb97b4e 100644 --- a/homeassistant/auth/__init__.py +++ b/homeassistant/auth/__init__.py @@ -211,7 +211,7 @@ def async_get_access_token(self, token): return tkn - async def _async_create_login_flow(self, handler, *, source, data): + async def _async_create_login_flow(self, handler, *, context, data): """Create a login flow.""" auth_provider = self._providers[handler] diff --git a/homeassistant/components/cast/__init__.py b/homeassistant/components/cast/__init__.py index aadf0103c5a6fe..6885f24269a07e 100644 --- a/homeassistant/components/cast/__init__.py +++ b/homeassistant/components/cast/__init__.py @@ -1,5 +1,5 @@ """Component to embed Google Cast.""" -from homeassistant import data_entry_flow +from homeassistant import config_entries from homeassistant.helpers import config_entry_flow @@ -15,7 +15,7 @@ async def async_setup(hass, config): if conf is not None: hass.async_create_task(hass.config_entries.flow.async_init( - DOMAIN, source=data_entry_flow.SOURCE_IMPORT)) + DOMAIN, context={'source': config_entries.SOURCE_IMPORT})) return True diff --git a/homeassistant/components/config/config_entries.py b/homeassistant/components/config/config_entries.py index 648f6ae9972c51..57fdbd31d20010 100644 --- a/homeassistant/components/config/config_entries.py +++ b/homeassistant/components/config/config_entries.py @@ -96,7 +96,7 @@ def get(self, request): return self.json([ flw for flw in hass.config_entries.flow.async_progress() - if flw['source'] != data_entry_flow.SOURCE_USER]) + if flw['source'] != config_entries.SOURCE_USER]) class ConfigManagerFlowResourceView(FlowManagerResourceView): diff --git a/homeassistant/components/deconz/__init__.py b/homeassistant/components/deconz/__init__.py index eacb31e3f8b171..eacfe22e818c01 100644 --- a/homeassistant/components/deconz/__init__.py +++ b/homeassistant/components/deconz/__init__.py @@ -6,6 +6,7 @@ """ import voluptuous as vol +from homeassistant import config_entries from homeassistant.const import ( CONF_API_KEY, CONF_EVENT, CONF_HOST, CONF_ID, CONF_PORT, EVENT_HOMEASSISTANT_STOP) @@ -60,7 +61,9 @@ async def async_setup(hass, config): deconz_config = config[DOMAIN] if deconz_config and not configured_hosts(hass): hass.async_add_job(hass.config_entries.flow.async_init( - DOMAIN, source='import', data=deconz_config + DOMAIN, + context={'source': config_entries.SOURCE_IMPORT}, + data=deconz_config )) return True diff --git a/homeassistant/components/deconz/config_flow.py b/homeassistant/components/deconz/config_flow.py index a6f675062275f5..fb2eb54232a1c7 100644 --- a/homeassistant/components/deconz/config_flow.py +++ b/homeassistant/components/deconz/config_flow.py @@ -33,6 +33,10 @@ def __init__(self): self.bridges = [] self.deconz_config = {} + async def async_step_user(self, user_input=None): + """Handle a flow initialized by the user.""" + return await self.async_step_init(user_input) + async def async_step_init(self, user_input=None): """Handle a deCONZ config flow start. diff --git a/homeassistant/components/discovery.py b/homeassistant/components/discovery.py index 8272fa9814aea1..b400d1d88855d5 100644 --- a/homeassistant/components/discovery.py +++ b/homeassistant/components/discovery.py @@ -13,7 +13,7 @@ import voluptuous as vol -from homeassistant import data_entry_flow +from homeassistant import config_entries from homeassistant.core import callback from homeassistant.const import EVENT_HOMEASSISTANT_START import homeassistant.helpers.config_validation as cv @@ -138,7 +138,7 @@ async def new_service_found(service, info): if service in CONFIG_ENTRY_HANDLERS: await hass.config_entries.flow.async_init( CONFIG_ENTRY_HANDLERS[service], - source=data_entry_flow.SOURCE_DISCOVERY, + context={'source': config_entries.SOURCE_DISCOVERY}, data=info ) return diff --git a/homeassistant/components/homematicip_cloud/__init__.py b/homeassistant/components/homematicip_cloud/__init__.py index b9266322978079..f2cc8f443ac801 100644 --- a/homeassistant/components/homematicip_cloud/__init__.py +++ b/homeassistant/components/homematicip_cloud/__init__.py @@ -10,6 +10,7 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv +from homeassistant import config_entries from .const import ( DOMAIN, HMIPC_HAPID, HMIPC_AUTHTOKEN, HMIPC_NAME, @@ -41,7 +42,8 @@ async def async_setup(hass, config): for conf in accesspoints: if conf[CONF_ACCESSPOINT] not in configured_haps(hass): hass.async_add_job(hass.config_entries.flow.async_init( - DOMAIN, source='import', data={ + DOMAIN, context={'source': config_entries.SOURCE_IMPORT}, + data={ HMIPC_HAPID: conf[CONF_ACCESSPOINT], HMIPC_AUTHTOKEN: conf[CONF_AUTHTOKEN], HMIPC_NAME: conf[CONF_NAME], diff --git a/homeassistant/components/homematicip_cloud/config_flow.py b/homeassistant/components/homematicip_cloud/config_flow.py index 3be89172e27fee..78970031d11c27 100644 --- a/homeassistant/components/homematicip_cloud/config_flow.py +++ b/homeassistant/components/homematicip_cloud/config_flow.py @@ -27,6 +27,10 @@ def __init__(self): """Initialize HomematicIP Cloud config flow.""" self.auth = None + async def async_step_user(self, user_input=None): + """Handle a flow initialized by the user.""" + return await self.async_step_init(user_input) + async def async_step_init(self, user_input=None): """Handle a flow start.""" errors = {} diff --git a/homeassistant/components/hue/__init__.py b/homeassistant/components/hue/__init__.py index dbd86ef31f344d..c04380e13035cf 100644 --- a/homeassistant/components/hue/__init__.py +++ b/homeassistant/components/hue/__init__.py @@ -9,7 +9,7 @@ import voluptuous as vol -from homeassistant import data_entry_flow +from homeassistant import config_entries from homeassistant.const import CONF_FILENAME, CONF_HOST from homeassistant.helpers import aiohttp_client, config_validation as cv @@ -108,7 +108,8 @@ async def async_setup(hass, config): # 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( - DOMAIN, source=data_entry_flow.SOURCE_IMPORT, data={ + DOMAIN, context={'source': config_entries.SOURCE_IMPORT}, + data={ 'host': bridge_conf[CONF_HOST], 'path': bridge_conf[CONF_FILENAME], } diff --git a/homeassistant/components/hue/bridge.py b/homeassistant/components/hue/bridge.py index b7cf0e1de07d3a..874c18aaa7ece1 100644 --- a/homeassistant/components/hue/bridge.py +++ b/homeassistant/components/hue/bridge.py @@ -51,7 +51,8 @@ async def async_setup(self, tries=0): # linking procedure. When linking succeeds, it will remove the # old config entry. hass.async_add_job(hass.config_entries.flow.async_init( - DOMAIN, source='import', data={ + DOMAIN, context={'source': config_entries.SOURCE_IMPORT}, + data={ 'host': host, } )) diff --git a/homeassistant/components/hue/config_flow.py b/homeassistant/components/hue/config_flow.py index a7fe3ff04e0a4a..49ebbdaabf5dbb 100644 --- a/homeassistant/components/hue/config_flow.py +++ b/homeassistant/components/hue/config_flow.py @@ -50,6 +50,10 @@ def __init__(self): """Initialize the Hue flow.""" self.host = None + async def async_step_user(self, user_input=None): + """Handle a flow initialized by the user.""" + return await self.async_step_init(user_input) + async def async_step_init(self, user_input=None): """Handle a flow start.""" from aiohue.discovery import discover_nupnp diff --git a/homeassistant/components/nest/__init__.py b/homeassistant/components/nest/__init__.py index 1adb113bb81f94..de9783ba931d26 100644 --- a/homeassistant/components/nest/__init__.py +++ b/homeassistant/components/nest/__init__.py @@ -11,6 +11,7 @@ import voluptuous as vol +from homeassistant import config_entries from homeassistant.const import ( CONF_STRUCTURE, CONF_FILENAME, CONF_BINARY_SENSORS, CONF_SENSORS, CONF_MONITORED_CONDITIONS, @@ -103,7 +104,8 @@ async def async_setup(hass, config): access_token_cache_file = hass.config.path(filename) hass.async_add_job(hass.config_entries.flow.async_init( - DOMAIN, source='import', data={ + DOMAIN, context={'source': config_entries.SOURCE_IMPORT}, + data={ 'nest_conf_path': access_token_cache_file, } )) diff --git a/homeassistant/components/nest/config_flow.py b/homeassistant/components/nest/config_flow.py index f97e0dc8ff5cf1..c9987693b1af75 100644 --- a/homeassistant/components/nest/config_flow.py +++ b/homeassistant/components/nest/config_flow.py @@ -58,6 +58,10 @@ def __init__(self): """Initialize the Nest config flow.""" self.flow_impl = None + async def async_step_user(self, user_input=None): + """Handle a flow initialized by the user.""" + return await self.async_step_init(user_input) + async def async_step_init(self, user_input=None): """Handle a flow start.""" flows = self.hass.data.get(DATA_FLOW_IMPL, {}) diff --git a/homeassistant/components/sonos/__init__.py b/homeassistant/components/sonos/__init__.py index 4c5592c02c22a2..bbc05a3aa6116f 100644 --- a/homeassistant/components/sonos/__init__.py +++ b/homeassistant/components/sonos/__init__.py @@ -1,5 +1,5 @@ """Component to embed Sonos.""" -from homeassistant import data_entry_flow +from homeassistant import config_entries from homeassistant.helpers import config_entry_flow @@ -15,7 +15,7 @@ async def async_setup(hass, config): if conf is not None: hass.async_create_task(hass.config_entries.flow.async_init( - DOMAIN, source=data_entry_flow.SOURCE_IMPORT)) + DOMAIN, context={'source': config_entries.SOURCE_IMPORT})) return True diff --git a/homeassistant/components/zone/config_flow.py b/homeassistant/components/zone/config_flow.py index 5ec955a48d90c0..01577de4c8f0a1 100644 --- a/homeassistant/components/zone/config_flow.py +++ b/homeassistant/components/zone/config_flow.py @@ -29,6 +29,10 @@ def __init__(self): """Initialize zone configuration flow.""" pass + async def async_step_user(self, user_input=None): + """Handle a flow initialized by the user.""" + return await self.async_step_init(user_input) + async def async_step_init(self, user_input=None): """Handle a flow start.""" errors = {} diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 12420e989eea74..51114a2a416917 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -24,20 +24,24 @@ dependencies and install the requirements of the component. At a minimum, each config flow will have to define a version number and the -'init' step. +'user' step. @config_entries.HANDLERS.register(DOMAIN) - class ExampleConfigFlow(config_entries.FlowHandler): + class ExampleConfigFlow(data_entry_flow.FlowHandler): VERSION = 1 - async def async_step_init(self, user_input=None): + async def async_step_user(self, user_input=None): … -The 'init' step is the first step of a flow and is called when a user +The 'user' step is the first step of a flow and is called when a user starts a new flow. Each step has three different possible results: "Show Form", "Abort" and "Create Entry". +> Note: prior 0.76, the default step is 'init' step, some config flows still +keep 'init' step to avoid break localization. All new config flow should use +'user' step. + ### Show Form This will show a form to the user to fill in. You define the current step, @@ -50,7 +54,7 @@ async def async_step_init(self, user_input=None): data_schema[vol.Required('password')] = str return self.async_show_form( - step_id='init', + step_id='user', title='Account Info', data_schema=vol.Schema(data_schema) ) @@ -97,10 +101,10 @@ async def async_step_init(self, user_input=None): You might want to initialize a config flow programmatically. For example, if we discover a device on the network that requires user interaction to finish setup. To do so, pass a source parameter and optional user input to the init -step: +method: await hass.config_entries.flow.async_init( - 'hue', source='discovery', data=discovery_info) + 'hue', context={'source': 'discovery'}, data=discovery_info) The config flow handler will need to add a step to support the source. The step should follow the same return values as a normal step. @@ -123,6 +127,11 @@ async def async_step_discovery(info): _LOGGER = logging.getLogger(__name__) + +SOURCE_USER = 'user' +SOURCE_DISCOVERY = 'discovery' +SOURCE_IMPORT = 'import' + HANDLERS = Registry() # Components that have config flows. In future we will auto-generate this list. FLOWS = [ @@ -151,8 +160,8 @@ async def async_step_discovery(info): DISCOVERY_NOTIFICATION_ID = 'config_entry_discovery' DISCOVERY_SOURCES = ( - data_entry_flow.SOURCE_DISCOVERY, - data_entry_flow.SOURCE_IMPORT, + SOURCE_DISCOVERY, + SOURCE_IMPORT, ) EVENT_FLOW_DISCOVERED = 'config_entry_discovered' @@ -374,12 +383,15 @@ async def _async_finish_flow(self, result): if result['type'] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY: return None + source = result['source'] + if source is None: + source = SOURCE_USER entry = ConfigEntry( version=result['version'], domain=result['handler'], title=result['title'], data=result['data'], - source=result['source'], + source=source, ) self._entries.append(entry) await self._async_schedule_save() @@ -399,17 +411,22 @@ async def _async_finish_flow(self, result): return entry - async def _async_create_flow(self, handler, *, source, data): + async def _async_create_flow(self, handler_key, *, context, data): """Create a flow for specified handler. Handler key is the domain of the component that we want to setup. """ - component = getattr(self.hass.components, handler) - handler = HANDLERS.get(handler) + component = getattr(self.hass.components, handler_key) + handler = HANDLERS.get(handler_key) if handler is None: raise data_entry_flow.UnknownHandler + if context is not None: + source = context.get('source', SOURCE_USER) + else: + source = SOURCE_USER + # Make sure requirements and dependencies of component are resolved await async_process_deps_reqs( self.hass, self._hass_config, handler, component) @@ -424,7 +441,10 @@ async def _async_create_flow(self, handler, *, source, data): notification_id=DISCOVERY_NOTIFICATION_ID ) - return handler() + flow = handler() + flow.source = source + flow.init_step = source + return flow async def _async_schedule_save(self): """Save the entity registry to a file.""" diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index f010ada02f3c7f..aee215dff8093b 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -8,10 +8,6 @@ _LOGGER = logging.getLogger(__name__) -SOURCE_USER = 'user' -SOURCE_DISCOVERY = 'discovery' -SOURCE_IMPORT = 'import' - RESULT_TYPE_FORM = 'form' RESULT_TYPE_CREATE_ENTRY = 'create_entry' RESULT_TYPE_ABORT = 'abort' @@ -53,22 +49,17 @@ def async_progress(self) -> List[Dict]: 'source': flow.source, } for flow in self._progress.values()] - async def async_init(self, handler: Callable, *, source: str = SOURCE_USER, - data: str = None) -> Any: + async def async_init(self, handler: Callable, *, context: Dict = None, + data: Any = None) -> Any: """Start a configuration flow.""" - flow = await self._async_create_flow(handler, source=source, data=data) + flow = await self._async_create_flow( + handler, context=context, data=data) flow.hass = self.hass flow.handler = handler flow.flow_id = uuid.uuid4().hex - flow.source = source self._progress[flow.flow_id] = flow - if source == SOURCE_USER: - step = 'init' - else: - step = source - - return await self._async_handle_step(flow, step, data) + return await self._async_handle_step(flow, flow.init_step, data) async def async_configure( self, flow_id: str, user_input: str = None) -> Any: @@ -131,9 +122,12 @@ class FlowHandler: flow_id = None hass = None handler = None - source = SOURCE_USER + source = None cur_step = None + # Set by _async_create_flow callback + init_step = 'init' + # Set by developer VERSION = 1 diff --git a/homeassistant/helpers/config_entry_flow.py b/homeassistant/helpers/config_entry_flow.py index 6f51d9aca2c49c..e17d5071c6a502 100644 --- a/homeassistant/helpers/config_entry_flow.py +++ b/homeassistant/helpers/config_entry_flow.py @@ -22,7 +22,7 @@ def __init__(self, domain, title, discovery_function): self._title = title self._discovery_function = discovery_function - async def async_step_init(self, user_input=None): + async def async_step_user(self, user_input=None): """Handle a flow initialized by the user.""" if self._async_current_entries(): return self.async_abort( diff --git a/homeassistant/helpers/data_entry_flow.py b/homeassistant/helpers/data_entry_flow.py index 4f412eb58e79ff..378febf8f6d623 100644 --- a/homeassistant/helpers/data_entry_flow.py +++ b/homeassistant/helpers/data_entry_flow.py @@ -2,7 +2,7 @@ import voluptuous as vol -from homeassistant import data_entry_flow +from homeassistant import data_entry_flow, config_entries from homeassistant.components.http import HomeAssistantView from homeassistant.components.http.data_validator import RequestDataValidator @@ -53,7 +53,8 @@ async def post(self, request, data): handler = data['handler'] try: - result = await self._flow_mgr.async_init(handler) + result = await self._flow_mgr.async_init( + handler, context={'source': config_entries.SOURCE_USER}) except data_entry_flow.UnknownHandler: return self.json_message('Invalid handler specified', 404) except data_entry_flow.UnknownStep: diff --git a/tests/common.py b/tests/common.py index 5567a431e58f12..3a2248d0d50528 100644 --- a/tests/common.py +++ b/tests/common.py @@ -12,7 +12,7 @@ import threading from contextlib import contextmanager -from homeassistant import auth, core as ha, data_entry_flow, config_entries +from homeassistant import auth, core as ha, config_entries from homeassistant.auth import ( models as auth_models, auth_store, providers as auth_providers) from homeassistant.setup import setup_component, async_setup_component @@ -509,7 +509,7 @@ class MockConfigEntry(config_entries.ConfigEntry): """Helper for creating config entries that adds some defaults.""" def __init__(self, *, domain='test', data=None, version=0, entry_id=None, - source=data_entry_flow.SOURCE_USER, title='Mock Title', + source=config_entries.SOURCE_USER, title='Mock Title', state=None): """Initialize a mock config entry.""" kwargs = { diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index 82c747da01c750..f85d7df1a86517 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -102,13 +102,13 @@ def test_initialize_flow(hass, client): """Test we can initialize a flow.""" class TestFlow(FlowHandler): @asyncio.coroutine - def async_step_init(self, user_input=None): + def async_step_user(self, user_input=None): schema = OrderedDict() schema[vol.Required('username')] = str schema[vol.Required('password')] = str return self.async_show_form( - step_id='init', + step_id='user', data_schema=schema, description_placeholders={ 'url': 'https://example.com', @@ -130,7 +130,7 @@ def async_step_init(self, user_input=None): assert data == { 'type': 'form', 'handler': 'test', - 'step_id': 'init', + 'step_id': 'user', 'data_schema': [ { 'name': 'username', @@ -157,7 +157,7 @@ def test_abort(hass, client): """Test a flow that aborts.""" class TestFlow(FlowHandler): @asyncio.coroutine - def async_step_init(self, user_input=None): + def async_step_user(self, user_input=None): return self.async_abort(reason='bla') with patch.dict(HANDLERS, {'test': TestFlow}): @@ -185,7 +185,7 @@ class TestFlow(FlowHandler): VERSION = 1 @asyncio.coroutine - def async_step_init(self, user_input=None): + def async_step_user(self, user_input=None): return self.async_create_entry( title='Test Entry', data={'secret': 'account_token'} @@ -218,7 +218,7 @@ class TestFlow(FlowHandler): VERSION = 1 @asyncio.coroutine - def async_step_init(self, user_input=None): + def async_step_user(self, user_input=None): return self.async_show_form( step_id='account', data_schema=vol.Schema({ @@ -286,7 +286,7 @@ def async_step_account(self, user_input=None): with patch.dict(HANDLERS, {'test': TestFlow}): form = yield from hass.config_entries.flow.async_init( - 'test', source='hassio') + 'test', context={'source': 'hassio'}) resp = yield from client.get('/api/config/config_entries/flow') assert resp.status == 200 @@ -305,13 +305,13 @@ def test_get_progress_flow(hass, client): """Test we can query the API for same result as we get from init a flow.""" class TestFlow(FlowHandler): @asyncio.coroutine - def async_step_init(self, user_input=None): + def async_step_user(self, user_input=None): schema = OrderedDict() schema[vol.Required('username')] = str schema[vol.Required('password')] = str return self.async_show_form( - step_id='init', + step_id='user', data_schema=schema, errors={ 'username': 'Should be unique.' diff --git a/tests/components/test_discovery.py b/tests/components/test_discovery.py index dd22c87cb18058..8b997cb911cf62 100644 --- a/tests/components/test_discovery.py +++ b/tests/components/test_discovery.py @@ -5,7 +5,7 @@ import pytest -from homeassistant import data_entry_flow +from homeassistant import config_entries from homeassistant.bootstrap import async_setup_component from homeassistant.components import discovery from homeassistant.util.dt import utcnow @@ -175,5 +175,5 @@ def discover(netdisco): assert len(m_init.mock_calls) == 1 args, kwargs = m_init.mock_calls[0][1:] assert args == ('mock-component',) - assert kwargs['source'] == data_entry_flow.SOURCE_DISCOVERY + assert kwargs['context']['source'] == config_entries.SOURCE_DISCOVERY assert kwargs['data'] == discovery_info diff --git a/tests/helpers/test_config_entry_flow.py b/tests/helpers/test_config_entry_flow.py index 19185e165bccd7..46c58320d504b2 100644 --- a/tests/helpers/test_config_entry_flow.py +++ b/tests/helpers/test_config_entry_flow.py @@ -31,7 +31,7 @@ async def test_single_entry_allowed(hass, flow_conf): flow.hass = hass MockConfigEntry(domain='test').add_to_hass(hass) - result = await flow.async_step_init() + result = await flow.async_step_user() assert result['type'] == data_entry_flow.RESULT_TYPE_ABORT assert result['reason'] == 'single_instance_allowed' @@ -42,7 +42,7 @@ async def test_user_no_devices_found(hass, flow_conf): flow = config_entries.HANDLERS['test']() flow.hass = hass - result = await flow.async_step_init() + result = await flow.async_step_user() assert result['type'] == data_entry_flow.RESULT_TYPE_ABORT assert result['reason'] == 'no_devices_found' @@ -54,7 +54,7 @@ async def test_user_no_confirmation(hass, flow_conf): flow.hass = hass flow_conf['discovered'] = True - result = await flow.async_step_init() + result = await flow.async_step_user() assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY @@ -90,12 +90,12 @@ async def test_multiple_discoveries(hass, flow_conf): loader.set_component(hass, 'test', MockModule('test')) result = await hass.config_entries.flow.async_init( - 'test', source=data_entry_flow.SOURCE_DISCOVERY, data={}) + 'test', context={'source': config_entries.SOURCE_DISCOVERY}, data={}) assert result['type'] == data_entry_flow.RESULT_TYPE_FORM # Second discovery result = await hass.config_entries.flow.async_init( - 'test', source=data_entry_flow.SOURCE_DISCOVERY, data={}) + 'test', context={'source': config_entries.SOURCE_DISCOVERY}, data={}) assert result['type'] == data_entry_flow.RESULT_TYPE_ABORT @@ -105,7 +105,7 @@ async def test_user_init_trumps_discovery(hass, flow_conf): # Discovery starts flow result = await hass.config_entries.flow.async_init( - 'test', source=data_entry_flow.SOURCE_DISCOVERY, data={}) + 'test', context={'source': config_entries.SOURCE_DISCOVERY}, data={}) assert result['type'] == data_entry_flow.RESULT_TYPE_FORM # User starts flow diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index d7a7ec4b82bf90..8ac4c642b0a83d 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -108,7 +108,7 @@ class TestFlow(data_entry_flow.FlowHandler): VERSION = 1 @asyncio.coroutine - def async_step_init(self, user_input=None): + def async_step_user(self, user_input=None): return self.async_create_entry( title='title', data={ @@ -162,7 +162,7 @@ class TestFlow(data_entry_flow.FlowHandler): VERSION = 5 @asyncio.coroutine - def async_step_init(self, user_input=None): + def async_step_user(self, user_input=None): return self.async_create_entry( title='Test Title', data={ @@ -177,7 +177,7 @@ class Test2Flow(data_entry_flow.FlowHandler): VERSION = 3 @asyncio.coroutine - def async_step_init(self, user_input=None): + def async_step_user(self, user_input=None): return self.async_create_entry( title='Test 2 Title', data={ @@ -266,7 +266,7 @@ async def async_step_discovery(self, user_input=None): with patch.dict(config_entries.HANDLERS, {'test': TestFlow}): result = await hass.config_entries.flow.async_init( - 'test', source=data_entry_flow.SOURCE_DISCOVERY) + 'test', context={'source': config_entries.SOURCE_DISCOVERY}) await hass.async_block_till_done() state = hass.states.get('persistent_notification.config_entry_discovery') @@ -294,7 +294,7 @@ async def async_step_discovery(self, user_input=None): with patch.dict(config_entries.HANDLERS, {'test': TestFlow}): await hass.config_entries.flow.async_init( - 'test', source=data_entry_flow.SOURCE_DISCOVERY) + 'test', context={'source': config_entries.SOURCE_DISCOVERY}) await hass.async_block_till_done() state = hass.states.get('persistent_notification.config_entry_discovery') diff --git a/tests/test_data_entry_flow.py b/tests/test_data_entry_flow.py index 894fd4d7194b14..dc10f3d8d1ae59 100644 --- a/tests/test_data_entry_flow.py +++ b/tests/test_data_entry_flow.py @@ -12,13 +12,18 @@ def manager(): handlers = Registry() entries = [] - async def async_create_flow(handler_name, *, source, data): + async def async_create_flow(handler_name, *, context, data): handler = handlers.get(handler_name) if handler is None: raise data_entry_flow.UnknownHandler - return handler() + flow = handler() + flow.init_step = context.get('init_step', 'init') \ + if context is not None else 'init' + flow.source = context.get('source') \ + if context is not None else 'user_input' + return flow async def async_add_entry(result): if (result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY): @@ -57,12 +62,12 @@ async def test_configure_two_steps(manager): class TestFlow(data_entry_flow.FlowHandler): VERSION = 1 - async def async_step_init(self, user_input=None): + async def async_step_first(self, user_input=None): if user_input is not None: self.init_data = user_input return await self.async_step_second() return self.async_show_form( - step_id='init', + step_id='first', data_schema=vol.Schema([str]) ) @@ -77,7 +82,7 @@ async def async_step_second(self, user_input=None): data_schema=vol.Schema([str]) ) - form = await manager.async_init('test') + form = await manager.async_init('test', context={'init_step': 'first'}) with pytest.raises(vol.Invalid): form = await manager.async_configure( @@ -163,7 +168,7 @@ async def async_step_init(self, user_input=None): assert entry['handler'] == 'test' assert entry['title'] == 'Test Title' assert entry['data'] == 'Test Data' - assert entry['source'] == data_entry_flow.SOURCE_USER + assert entry['source'] == 'user_input' async def test_discovery_init_flow(manager): @@ -172,7 +177,7 @@ async def test_discovery_init_flow(manager): class TestFlow(data_entry_flow.FlowHandler): VERSION = 5 - async def async_step_discovery(self, info): + async def async_step_init(self, info): return self.async_create_entry(title=info['id'], data=info) data = { @@ -181,7 +186,7 @@ async def async_step_discovery(self, info): } await manager.async_init( - 'test', source=data_entry_flow.SOURCE_DISCOVERY, data=data) + 'test', context={'source': 'discovery'}, data=data) assert len(manager.async_progress()) == 0 assert len(manager.mock_created_entries) == 1 @@ -190,4 +195,4 @@ async def async_step_discovery(self, info): assert entry['handler'] == 'test' assert entry['title'] == 'hello' assert entry['data'] == data - assert entry['source'] == data_entry_flow.SOURCE_DISCOVERY + assert entry['source'] == 'discovery' From 2233d7ca98f500b9850fc9163e34eb4911c18fc4 Mon Sep 17 00:00:00 2001 From: Jason Hu Date: Thu, 9 Aug 2018 04:31:48 -0700 Subject: [PATCH 074/117] Fix downgrade hassio cannot get refresh_token issue (#15874) * Fix downgrade hassio issue * Update __init__.py --- homeassistant/components/hassio/__init__.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index 3a3e19fb48435c..13c486533d9975 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -175,10 +175,13 @@ def async_setup(hass, config): if data is None: data = {} + refresh_token = None if 'hassio_user' in data: user = yield from hass.auth.async_get_user(data['hassio_user']) - refresh_token = list(user.refresh_tokens.values())[0] - else: + if user: + 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) data['hassio_user'] = user.id From a29f86790842d9677bc73454a677c498a2bc6d5f Mon Sep 17 00:00:00 2001 From: Mattias Welponer Date: Thu, 9 Aug 2018 14:43:13 +0200 Subject: [PATCH 075/117] Add HomematicIP Cloud smoke detector device (#15621) * Add smoke detector device * Remove not needed __init__ functions --- .../binary_sensor/homematicip_cloud.py | 36 +++++++++++-------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/binary_sensor/homematicip_cloud.py b/homeassistant/components/binary_sensor/homematicip_cloud.py index 6966f61129c1df..1ab4fe74d695da 100644 --- a/homeassistant/components/binary_sensor/homematicip_cloud.py +++ b/homeassistant/components/binary_sensor/homematicip_cloud.py @@ -16,10 +16,7 @@ _LOGGER = logging.getLogger(__name__) -ATTR_WINDOW_STATE = 'window_state' -ATTR_EVENT_DELAY = 'event_delay' -ATTR_MOTION_DETECTED = 'motion_detected' -ATTR_ILLUMINATION = 'illumination' +STATE_SMOKE_OFF = 'IDLE_OFF' async def async_setup_platform(hass, config, async_add_devices, @@ -30,15 +27,18 @@ async def async_setup_platform(hass, config, async_add_devices, async def async_setup_entry(hass, config_entry, async_add_devices): """Set up the HomematicIP binary sensor from a config entry.""" - from homematicip.device import (ShutterContact, MotionDetectorIndoor) + from homematicip.aio.device import ( + AsyncShutterContact, AsyncMotionDetectorIndoor, AsyncSmokeDetector) home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home devices = [] for device in home.devices: - if isinstance(device, ShutterContact): + if isinstance(device, AsyncShutterContact): devices.append(HomematicipShutterContact(home, device)) - elif isinstance(device, MotionDetectorIndoor): + elif isinstance(device, AsyncMotionDetectorIndoor): devices.append(HomematicipMotionDetector(home, device)) + elif isinstance(device, AsyncSmokeDetector): + devices.append(HomematicipSmokeDetector(home, device)) if devices: async_add_devices(devices) @@ -47,10 +47,6 @@ async def async_setup_entry(hass, config_entry, async_add_devices): class HomematicipShutterContact(HomematicipGenericDevice, BinarySensorDevice): """HomematicIP shutter contact.""" - def __init__(self, home, device): - """Initialize the shutter contact.""" - super().__init__(home, device) - @property def device_class(self): """Return the class of this sensor.""" @@ -71,10 +67,6 @@ def is_on(self): class HomematicipMotionDetector(HomematicipGenericDevice, BinarySensorDevice): """MomematicIP motion detector.""" - def __init__(self, home, device): - """Initialize the shutter contact.""" - super().__init__(home, device) - @property def device_class(self): """Return the class of this sensor.""" @@ -86,3 +78,17 @@ def is_on(self): if self._device.sabotage: return True return self._device.motionDetected + + +class HomematicipSmokeDetector(HomematicipGenericDevice, BinarySensorDevice): + """MomematicIP smoke detector.""" + + @property + def device_class(self): + """Return the class of this sensor.""" + return 'smoke' + + @property + def is_on(self): + """Return true if smoke is detected.""" + return self._device.smokeDetectorAlarmType != STATE_SMOKE_OFF From 86658f310d0c6579c706bce1013e08a42d507609 Mon Sep 17 00:00:00 2001 From: rafale77 Date: Thu, 9 Aug 2018 21:59:23 +0800 Subject: [PATCH 076/117] Fix for multiple camera switches naming of entity (#14028) * Fix for multiple camera switches naming of entity appended camera name to the switch entity name. * Update amcrest.py * Update amcrest.py * Update amcrest.py * Update amcrest.py * Update amcrest.py * Update amcrest.py * Update amcrest.py * Update amcrest.py * Update amcrest.py * Update amcrest.py * Update amcrest.py * Add digest authentification * Update rest_command.py * Update config.py * Update rest_command.py * Update config.py --- homeassistant/components/switch/amcrest.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/switch/amcrest.py b/homeassistant/components/switch/amcrest.py index 0b93bc98b1029c..cfe33562f9f987 100644 --- a/homeassistant/components/switch/amcrest.py +++ b/homeassistant/components/switch/amcrest.py @@ -30,7 +30,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): all_switches = [] for setting in switches: - all_switches.append(AmcrestSwitch(setting, camera)) + all_switches.append(AmcrestSwitch(setting, camera, name)) async_add_devices(all_switches, True) @@ -38,11 +38,11 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): class AmcrestSwitch(ToggleEntity): """Representation of an Amcrest IP camera switch.""" - def __init__(self, setting, camera): + def __init__(self, setting, camera, name): """Initialize the Amcrest switch.""" self._setting = setting self._camera = camera - self._name = SWITCHES[setting][0] + self._name = '{} {}'.format(SWITCHES[setting][0], name) self._icon = SWITCHES[setting][1] self._state = None From 664eae72d178302baa015e5cd9dbcb4543f03f81 Mon Sep 17 00:00:00 2001 From: mountainsandcode Date: Thu, 9 Aug 2018 16:27:29 +0200 Subject: [PATCH 077/117] Add realtime true/false switch for Waze (#15228) --- homeassistant/components/sensor/waze_travel_time.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sensor/waze_travel_time.py b/homeassistant/components/sensor/waze_travel_time.py index 70c169a1b7c982..023da72299b901 100644 --- a/homeassistant/components/sensor/waze_travel_time.py +++ b/homeassistant/components/sensor/waze_travel_time.py @@ -31,8 +31,10 @@ CONF_ORIGIN = 'origin' CONF_INCL_FILTER = 'incl_filter' CONF_EXCL_FILTER = 'excl_filter' +CONF_REALTIME = 'realtime' DEFAULT_NAME = 'Waze Travel Time' +DEFAULT_REALTIME = True ICON = 'mdi:car' @@ -49,6 +51,7 @@ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_INCL_FILTER): cv.string, vol.Optional(CONF_EXCL_FILTER): cv.string, + vol.Optional(CONF_REALTIME, default=DEFAULT_REALTIME): cv.boolean, }) @@ -60,9 +63,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None): region = config.get(CONF_REGION) incl_filter = config.get(CONF_INCL_FILTER) excl_filter = config.get(CONF_EXCL_FILTER) + realtime = config.get(CONF_REALTIME) sensor = WazeTravelTime(name, origin, destination, region, - incl_filter, excl_filter) + incl_filter, excl_filter, realtime) add_devices([sensor]) @@ -80,12 +84,13 @@ class WazeTravelTime(Entity): """Representation of a Waze travel time sensor.""" def __init__(self, name, origin, destination, region, - incl_filter, excl_filter): + incl_filter, excl_filter, realtime): """Initialize the Waze travel time sensor.""" self._name = name self._region = region self._incl_filter = incl_filter self._excl_filter = excl_filter + self._realtime = realtime self._state = None self._origin_entity_id = None self._destination_entity_id = None @@ -197,7 +202,7 @@ def update(self): try: params = WazeRouteCalculator.WazeRouteCalculator( self._origin, self._destination, self._region) - routes = params.calc_all_routes_info() + routes = params.calc_all_routes_info(real_time=self._realtime) if self._incl_filter is not None: routes = {k: v for k, v in routes.items() if From ef61c0c3a4f3246879c8c088af3529bd58005dd5 Mon Sep 17 00:00:00 2001 From: Benoit Louy Date: Thu, 9 Aug 2018 13:58:16 -0400 Subject: [PATCH 078/117] Add PJLink media player platform (#15083) * add pjlink media player component * retrieve pjlink device name from projector if name isn't specified in configuration * update .coveragerc * fix style * add missing docstrings * address PR comments from @MartinHjelmare * fix code style * use snake case string for source names * add missing period at the end of comment string * rewrite method as function * revert to use source name provided by projector --- .coveragerc | 1 + .../components/media_player/pjlink.py | 157 ++++++++++++++++++ requirements_all.txt | 3 + 3 files changed, 161 insertions(+) create mode 100644 homeassistant/components/media_player/pjlink.py diff --git a/.coveragerc b/.coveragerc index 1e358cd779158d..f06e9356d21e39 100644 --- a/.coveragerc +++ b/.coveragerc @@ -537,6 +537,7 @@ omit = homeassistant/components/media_player/pandora.py homeassistant/components/media_player/philips_js.py homeassistant/components/media_player/pioneer.py + homeassistant/components/media_player/pjlink.py homeassistant/components/media_player/plex.py homeassistant/components/media_player/roku.py homeassistant/components/media_player/russound_rio.py diff --git a/homeassistant/components/media_player/pjlink.py b/homeassistant/components/media_player/pjlink.py new file mode 100644 index 00000000000000..5d3122256ea670 --- /dev/null +++ b/homeassistant/components/media_player/pjlink.py @@ -0,0 +1,157 @@ +""" +Support for controlling projector via the PJLink protocol. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/media_player.pjlink/ +""" +import logging + +import voluptuous as vol + +from homeassistant.components.media_player import ( + SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, + SUPPORT_SELECT_SOURCE, PLATFORM_SCHEMA, MediaPlayerDevice) +from homeassistant.const import ( + STATE_OFF, STATE_ON, CONF_HOST, + CONF_NAME, CONF_PASSWORD, CONF_PORT) +import homeassistant.helpers.config_validation as cv + +REQUIREMENTS = ['pypjlink2==1.2.0'] + +_LOGGER = logging.getLogger(__name__) + +CONF_ENCODING = 'encoding' + +DEFAULT_PORT = 4352 +DEFAULT_ENCODING = 'utf-8' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_ENCODING, default=DEFAULT_ENCODING): cv.string, + vol.Optional(CONF_PASSWORD): cv.string, +}) + +SUPPORT_PJLINK = SUPPORT_VOLUME_MUTE | \ + SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_SELECT_SOURCE + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the PJLink platform.""" + host = config.get(CONF_HOST) + port = config.get(CONF_PORT) + name = config.get(CONF_NAME) + encoding = config.get(CONF_ENCODING) + password = config.get(CONF_PASSWORD) + + if 'pjlink' not in hass.data: + hass.data['pjlink'] = {} + hass_data = hass.data['pjlink'] + + device_label = "{}:{}".format(host, port) + if device_label in hass_data: + return + + device = PjLinkDevice(host, port, name, encoding, password) + hass_data[device_label] = device + add_devices([device], True) + + +def format_input_source(input_source_name, input_source_number): + """Format input source for display in UI.""" + return "{} {}".format(input_source_name, input_source_number) + + +class PjLinkDevice(MediaPlayerDevice): + """Representation of a PJLink device.""" + + def __init__(self, host, port, name, encoding, password): + """Iinitialize the PJLink device.""" + self._host = host + self._port = port + self._name = name + self._password = password + self._encoding = encoding + self._muted = False + self._pwstate = STATE_OFF + self._current_source = None + with self.projector() as projector: + if not self._name: + self._name = projector.get_name() + inputs = projector.get_inputs() + self._source_name_mapping = \ + {format_input_source(*x): x for x in inputs} + self._source_list = sorted(self._source_name_mapping.keys()) + + def projector(self): + """Create PJLink Projector instance.""" + from pypjlink import Projector + projector = Projector.from_address(self._host, self._port, + self._encoding) + projector.authenticate(self._password) + return projector + + def update(self): + """Get the latest state from the device.""" + with self.projector() as projector: + pwstate = projector.get_power() + if pwstate == 'off': + self._pwstate = STATE_OFF + else: + self._pwstate = STATE_ON + self._muted = projector.get_mute()[1] + self._current_source = \ + format_input_source(*projector.get_input()) + + @property + def name(self): + """Return the name of the device.""" + return self._name + + @property + def state(self): + """Return the state of the device.""" + return self._pwstate + + @property + def is_volume_muted(self): + """Return boolean indicating mute status.""" + return self._muted + + @property + def source(self): + """Return current input source.""" + return self._current_source + + @property + def source_list(self): + """Return all available input sources.""" + return self._source_list + + @property + def supported_features(self): + """Return projector supported features.""" + return SUPPORT_PJLINK + + def turn_off(self): + """Turn projector off.""" + with self.projector() as projector: + projector.set_power('off') + + def turn_on(self): + """Turn projector on.""" + with self.projector() as projector: + projector.set_power('on') + + def mute_volume(self, mute): + """Mute (true) of unmute (false) media player.""" + with self.projector() as projector: + from pypjlink import MUTE_AUDIO + projector.set_mute(MUTE_AUDIO, mute) + + def select_source(self, source): + """Set the input source.""" + source = self._source_name_mapping[source] + with self.projector() as projector: + projector.set_input(*source) diff --git a/requirements_all.txt b/requirements_all.txt index 6ec99e849536ef..c6b4898dc4c2e5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -984,6 +984,9 @@ pyotp==2.2.6 # homeassistant.components.weather.openweathermap pyowm==2.9.0 +# homeassistant.components.media_player.pjlink +pypjlink2==1.2.0 + # homeassistant.components.sensor.pollen pypollencom==2.1.0 From dc01b1726050fd379f544244d92e1b1c471ac37b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Thu, 9 Aug 2018 23:53:12 +0300 Subject: [PATCH 079/117] Some typing related fixes (#15899) * Fix FlowManager.async_init handler type It's not a Callable, but typically a key pointing to one in a dict. * Mark pip_kwargs return type hint as Any-valued dict install_package takes other than str args too. --- homeassistant/data_entry_flow.py | 4 ++-- homeassistant/requirements.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index aee215dff8093b..7609ffa615a357 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -2,7 +2,7 @@ import logging import uuid import voluptuous as vol -from typing import Dict, Any, Callable, List, Optional # noqa pylint: disable=unused-import +from typing import Dict, Any, Callable, Hashable, List, Optional # noqa pylint: disable=unused-import from .core import callback, HomeAssistant from .exceptions import HomeAssistantError @@ -49,7 +49,7 @@ def async_progress(self) -> List[Dict]: 'source': flow.source, } for flow in self._progress.values()] - async def async_init(self, handler: Callable, *, context: Dict = None, + async def async_init(self, handler: Hashable, *, context: Dict = None, data: Any = None) -> Any: """Start a configuration flow.""" flow = await self._async_create_flow( diff --git a/homeassistant/requirements.py b/homeassistant/requirements.py index b73ec4e184e87d..b9b5e137d5c108 100644 --- a/homeassistant/requirements.py +++ b/homeassistant/requirements.py @@ -3,7 +3,7 @@ from functools import partial import logging import os -from typing import List, Dict, Optional +from typing import Any, Dict, List, Optional import homeassistant.util.package as pkg_util from homeassistant.core import HomeAssistant @@ -37,7 +37,7 @@ async def async_process_requirements(hass: HomeAssistant, name: str, return True -def pip_kwargs(config_dir: Optional[str]) -> Dict[str, str]: +def pip_kwargs(config_dir: Optional[str]) -> Dict[str, Any]: """Return keyword arguments for PIP install.""" kwargs = { 'constraints': os.path.join(os.path.dirname(__file__), CONSTRAINT_FILE) From f98629b8953717e5baa0c5cab3946fd8030f86b9 Mon Sep 17 00:00:00 2001 From: Joe Lu Date: Thu, 9 Aug 2018 22:27:50 -0700 Subject: [PATCH 080/117] Update August component to use py-august:0.6.0 (#15916) --- homeassistant/components/august.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/august.py b/homeassistant/components/august.py index eb25ee8fb08219..5f268a95f5dcc9 100644 --- a/homeassistant/components/august.py +++ b/homeassistant/components/august.py @@ -21,7 +21,7 @@ _CONFIGURING = {} -REQUIREMENTS = ['py-august==0.4.0'] +REQUIREMENTS = ['py-august==0.6.0'] DEFAULT_TIMEOUT = 10 ACTIVITY_FETCH_LIMIT = 10 diff --git a/requirements_all.txt b/requirements_all.txt index c6b4898dc4c2e5..78146cbb77a4a8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -706,7 +706,7 @@ pushetta==1.0.15 pwmled==1.2.1 # homeassistant.components.august -py-august==0.4.0 +py-august==0.6.0 # homeassistant.components.canary py-canary==0.5.0 From 1911168855c4e04979a09fbb64c38e8645301e2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Fri, 10 Aug 2018 17:09:08 +0300 Subject: [PATCH 081/117] Misc cleanups (#15907) * device_tracker.huawei_router: Pylint logging-not-lazy fix * sensor.irish_rail_transport: Clean up redundant self.info test --- homeassistant/components/device_tracker/huawei_router.py | 3 +-- homeassistant/components/sensor/irish_rail_transport.py | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/device_tracker/huawei_router.py b/homeassistant/components/device_tracker/huawei_router.py index 804269e62280c5..f5e4fa8a7141a5 100644 --- a/homeassistant/components/device_tracker/huawei_router.py +++ b/homeassistant/components/device_tracker/huawei_router.py @@ -85,8 +85,7 @@ def _update_info(self): active_clients = [client for client in data if client.state] self.last_results = active_clients - # pylint: disable=logging-not-lazy - _LOGGER.debug("Active clients: " + "\n" + _LOGGER.debug("Active clients: %s", "\n" .join((client.mac + " " + client.name) for client in active_clients)) return True diff --git a/homeassistant/components/sensor/irish_rail_transport.py b/homeassistant/components/sensor/irish_rail_transport.py index 5febebeec870c3..38fd910260ad6d 100644 --- a/homeassistant/components/sensor/irish_rail_transport.py +++ b/homeassistant/components/sensor/irish_rail_transport.py @@ -164,7 +164,7 @@ def update(self): ATTR_TRAIN_TYPE: train.get('type')} self.info.append(train_data) - if not self.info or not self.info: + if not self.info: self.info = self._empty_train_data() def _empty_train_data(self): From b370b6a4e467af91b3e270b4f256df3ffbbff36a Mon Sep 17 00:00:00 2001 From: clayton craft Date: Fri, 10 Aug 2018 07:10:19 -0700 Subject: [PATCH 082/117] Update radiotherm to 1.4.1 (#15910) --- homeassistant/components/climate/radiotherm.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/climate/radiotherm.py b/homeassistant/components/climate/radiotherm.py index b3043689f8ccc0..c8441a9f7af0ea 100644 --- a/homeassistant/components/climate/radiotherm.py +++ b/homeassistant/components/climate/radiotherm.py @@ -18,7 +18,7 @@ CONF_HOST, TEMP_FAHRENHEIT, ATTR_TEMPERATURE, PRECISION_HALVES) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['radiotherm==1.3'] +REQUIREMENTS = ['radiotherm==1.4.1'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 78146cbb77a4a8..358225b69ea724 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1201,7 +1201,7 @@ qnapstats==0.2.6 rachiopy==0.1.3 # homeassistant.components.climate.radiotherm -radiotherm==1.3 +radiotherm==1.4.1 # homeassistant.components.raincloud raincloudy==0.0.5 From da916d7b27f86f18697ec6cb425909cc233921a2 Mon Sep 17 00:00:00 2001 From: Adam Mills Date: Fri, 10 Aug 2018 11:35:01 -0400 Subject: [PATCH 083/117] Fix bug in translations upload script (#15922) --- script/translations_upload_merge.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/translations_upload_merge.py b/script/translations_upload_merge.py index 450a4c9ba0fb64..ce0a14c85e6fa4 100755 --- a/script/translations_upload_merge.py +++ b/script/translations_upload_merge.py @@ -57,7 +57,7 @@ def get_translation_dict(translations, component, platform): if not component: return translations['component'] - if component not in translations: + if component not in translations['component']: translations['component'][component] = {} if not platform: From 9512bb95872a76d02df84fe5c40fcc9461f42878 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 10 Aug 2018 18:09:01 +0200 Subject: [PATCH 084/117] Add and restore context in recorder (#15859) --- .../components/recorder/migration.py | 34 +++++++++++++++++++ homeassistant/components/recorder/models.py | 33 ++++++++++++++---- homeassistant/core.py | 6 ++-- tests/common.py | 2 +- tests/components/recorder/test_models.py | 2 +- tests/components/test_history.py | 7 ++-- tests/test_core.py | 3 +- 7 files changed, 73 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index af70c9d998c57f..939985ebfb117d 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -114,6 +114,27 @@ def _drop_index(engine, table_name, index_name): "critical operation.", index_name, table_name) +def _add_columns(engine, table_name, columns_def): + """Add columns to a table.""" + from sqlalchemy import text + from sqlalchemy.exc import SQLAlchemyError + + columns_def = ['ADD COLUMN {}'.format(col_def) for col_def in columns_def] + + try: + engine.execute(text("ALTER TABLE {table} {columns_def}".format( + table=table_name, + columns_def=', '.join(columns_def)))) + return + except SQLAlchemyError: + pass + + for column_def in columns_def: + engine.execute(text("ALTER TABLE {table} {column_def}".format( + table=table_name, + column_def=column_def))) + + def _apply_update(engine, new_version, old_version): """Perform operations to bring schema up to date.""" if new_version == 1: @@ -146,6 +167,19 @@ def _apply_update(engine, new_version, old_version): elif new_version == 5: # Create supporting index for States.event_id foreign key _create_index(engine, "states", "ix_states_event_id") + elif new_version == 6: + _add_columns(engine, "events", [ + 'context_id CHARACTER(36)', + 'context_user_id CHARACTER(36)', + ]) + _create_index(engine, "events", "ix_events_context_id") + _create_index(engine, "events", "ix_events_context_user_id") + _add_columns(engine, "states", [ + 'context_id CHARACTER(36)', + 'context_user_id CHARACTER(36)', + ]) + _create_index(engine, "states", "ix_states_context_id") + _create_index(engine, "states", "ix_states_context_user_id") else: raise ValueError("No schema migration defined for version {}" .format(new_version)) diff --git a/homeassistant/components/recorder/models.py b/homeassistant/components/recorder/models.py index e79484462314d5..b8b777990f76b6 100644 --- a/homeassistant/components/recorder/models.py +++ b/homeassistant/components/recorder/models.py @@ -9,14 +9,15 @@ from sqlalchemy.ext.declarative import declarative_base import homeassistant.util.dt as dt_util -from homeassistant.core import Event, EventOrigin, State, split_entity_id +from homeassistant.core import ( + Context, Event, EventOrigin, State, split_entity_id) from homeassistant.remote import JSONEncoder # SQLAlchemy Schema # pylint: disable=invalid-name Base = declarative_base() -SCHEMA_VERSION = 5 +SCHEMA_VERSION = 6 _LOGGER = logging.getLogger(__name__) @@ -31,6 +32,8 @@ class Events(Base): # type: ignore origin = Column(String(32)) time_fired = Column(DateTime(timezone=True), index=True) created = Column(DateTime(timezone=True), default=datetime.utcnow) + context_id = Column(String(36), index=True) + context_user_id = Column(String(36), index=True) @staticmethod def from_event(event): @@ -38,16 +41,23 @@ def from_event(event): return Events(event_type=event.event_type, event_data=json.dumps(event.data, cls=JSONEncoder), origin=str(event.origin), - time_fired=event.time_fired) + time_fired=event.time_fired, + context_id=event.context.id, + context_user_id=event.context.user_id) def to_native(self): """Convert to a natve HA Event.""" + context = Context( + id=self.context_id, + user_id=self.context_user_id + ) try: return Event( self.event_type, json.loads(self.event_data), EventOrigin(self.origin), - _process_timestamp(self.time_fired) + _process_timestamp(self.time_fired), + context=context, ) except ValueError: # When json.loads fails @@ -69,6 +79,8 @@ class States(Base): # type: ignore last_updated = Column(DateTime(timezone=True), default=datetime.utcnow, index=True) created = Column(DateTime(timezone=True), default=datetime.utcnow) + context_id = Column(String(36), index=True) + context_user_id = Column(String(36), index=True) __table_args__ = ( # Used for fetching the state of entities at a specific time @@ -82,7 +94,11 @@ def from_event(event): entity_id = event.data['entity_id'] state = event.data.get('new_state') - dbstate = States(entity_id=entity_id) + dbstate = States( + entity_id=entity_id, + context_id=event.context.id, + context_user_id=event.context.user_id, + ) # State got deleted if state is None: @@ -103,12 +119,17 @@ def from_event(event): def to_native(self): """Convert to an HA state object.""" + context = Context( + id=self.context_id, + user_id=self.context_user_id + ) try: return State( self.entity_id, self.state, json.loads(self.attributes), _process_timestamp(self.last_changed), - _process_timestamp(self.last_updated) + _process_timestamp(self.last_updated), + context=context, ) except ValueError: # When json.loads fails diff --git a/homeassistant/core.py b/homeassistant/core.py index b17df2c11fecf1..cc027c6f5d0d01 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -423,7 +423,8 @@ def __eq__(self, other: Any) -> bool: self.event_type == other.event_type and self.data == other.data and self.origin == other.origin and - self.time_fired == other.time_fired) + self.time_fired == other.time_fired and + self.context == other.context) class EventBus: @@ -695,7 +696,8 @@ def __eq__(self, other: Any) -> bool: return (self.__class__ == other.__class__ and # type: ignore self.entity_id == other.entity_id and self.state == other.state and - self.attributes == other.attributes) + self.attributes == other.attributes and + self.context == other.context) def __repr__(self) -> str: """Return the representation of the states.""" diff --git a/tests/common.py b/tests/common.py index 3a2248d0d50528..df333cca735813 100644 --- a/tests/common.py +++ b/tests/common.py @@ -266,7 +266,7 @@ def mock_state_change_event(hass, new_state, old_state=None): if old_state: event_data['old_state'] = old_state - hass.bus.fire(EVENT_STATE_CHANGED, event_data) + hass.bus.fire(EVENT_STATE_CHANGED, event_data, context=new_state.context) @asyncio.coroutine diff --git a/tests/components/recorder/test_models.py b/tests/components/recorder/test_models.py index c616f3d0af1b9e..3d1beb3a642bdd 100644 --- a/tests/components/recorder/test_models.py +++ b/tests/components/recorder/test_models.py @@ -60,7 +60,7 @@ def test_from_event(self): 'entity_id': 'sensor.temperature', 'old_state': None, 'new_state': state, - }) + }, context=state.context) assert state == States.from_event(event).to_native() def test_from_event_to_delete_state(self): diff --git a/tests/components/test_history.py b/tests/components/test_history.py index 70f7152e07f7bd..b348498b07e25f 100644 --- a/tests/components/test_history.py +++ b/tests/components/test_history.py @@ -83,9 +83,10 @@ def test_get_states(self): self.wait_recording_done() # Get states returns everything before POINT - self.assertEqual(states, - sorted(history.get_states(self.hass, future), - key=lambda state: state.entity_id)) + for state1, state2 in zip( + states, sorted(history.get_states(self.hass, future), + key=lambda state: state.entity_id)): + assert state1 == state2 # Test get_state here because we have a DB setup self.assertEqual( diff --git a/tests/test_core.py b/tests/test_core.py index 9de801e0bb4f09..f23bed6bc8a028 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -246,8 +246,9 @@ def test_eq(self): """Test events.""" now = dt_util.utcnow() data = {'some': 'attr'} + context = ha.Context() event1, event2 = [ - ha.Event('some_type', data, time_fired=now) + ha.Event('some_type', data, time_fired=now, context=context) for _ in range(2) ] From 0ab3e7a92ac82f2a205e1b152c7ff2f15138a28c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 10 Aug 2018 18:09:42 +0200 Subject: [PATCH 085/117] Add IndieAuth 4.2.2 redirect uri at client id (#15911) * Add IndieAuth 4.2.2 redirect uri at client id * Fix tests * Add comment * Limit to first 10kB of each page --- homeassistant/components/auth/indieauth.py | 77 +++++++++++++++++-- homeassistant/components/auth/login_flow.py | 4 +- tests/components/auth/test_indieauth.py | 82 ++++++++++++++++----- tests/helpers/test_aiohttp_client.py | 7 +- tests/test_util/aiohttp.py | 29 ++++---- 5 files changed, 153 insertions(+), 46 deletions(-) diff --git a/homeassistant/components/auth/indieauth.py b/homeassistant/components/auth/indieauth.py index ef7f8a9b2927f1..48f7ab06ab4933 100644 --- a/homeassistant/components/auth/indieauth.py +++ b/homeassistant/components/auth/indieauth.py @@ -1,6 +1,10 @@ """Helpers to resolve client ID/secret.""" +import asyncio +from html.parser import HTMLParser from ipaddress import ip_address, ip_network -from urllib.parse import urlparse +from urllib.parse import urlparse, urljoin + +from aiohttp.client_exceptions import ClientError # IP addresses of loopback interfaces ALLOWED_IPS = ( @@ -16,7 +20,7 @@ ) -def verify_redirect_uri(client_id, redirect_uri): +async def verify_redirect_uri(hass, client_id, redirect_uri): """Verify that the client and redirect uri match.""" try: client_id_parts = _parse_client_id(client_id) @@ -25,16 +29,75 @@ def verify_redirect_uri(client_id, redirect_uri): redirect_parts = _parse_url(redirect_uri) - # IndieAuth 4.2.2 allows for redirect_uri to be on different domain - # but needs to be specified in link tag when fetching `client_id`. - # This is not implemented. - # Verify redirect url and client url have same scheme and domain. - return ( + is_valid = ( client_id_parts.scheme == redirect_parts.scheme and client_id_parts.netloc == redirect_parts.netloc ) + if is_valid: + return True + + # IndieAuth 4.2.2 allows for redirect_uri to be on different domain + # but needs to be specified in link tag when fetching `client_id`. + redirect_uris = await fetch_redirect_uris(hass, client_id) + return redirect_uri in redirect_uris + + +class LinkTagParser(HTMLParser): + """Parser to find link tags.""" + + def __init__(self, rel): + """Initialize a link tag parser.""" + super().__init__() + self.rel = rel + self.found = [] + + def handle_starttag(self, tag, attrs): + """Handle finding a start tag.""" + if tag != 'link': + return + + attrs = dict(attrs) + + if attrs.get('rel') == self.rel: + self.found.append(attrs.get('href')) + + +async def fetch_redirect_uris(hass, url): + """Find link tag with redirect_uri values. + + IndieAuth 4.2.2 + + The client SHOULD publish one or more tags or Link HTTP headers with + a rel attribute of redirect_uri at the client_id URL. + + We limit to the first 10kB of the page. + + We do not implement extracting redirect uris from headers. + """ + session = hass.helpers.aiohttp_client.async_get_clientsession() + parser = LinkTagParser('redirect_uri') + chunks = 0 + try: + resp = await session.get(url, timeout=5) + + async for data in resp.content.iter_chunked(1024): + parser.feed(data.decode()) + chunks += 1 + + if chunks == 10: + break + + except (asyncio.TimeoutError, ClientError): + pass + + # Authorization endpoints verifying that a redirect_uri is allowed for use + # by a client MUST look for an exact match of the given redirect_uri in the + # request against the list of redirect_uris discovered after resolving any + # relative URLs. + return [urljoin(url, found) for found in parser.found] + def verify_client_id(client_id): """Verify that the client id is valid.""" diff --git a/homeassistant/components/auth/login_flow.py b/homeassistant/components/auth/login_flow.py index bced421d6f982b..8b983b6d19f2b4 100644 --- a/homeassistant/components/auth/login_flow.py +++ b/homeassistant/components/auth/login_flow.py @@ -142,8 +142,8 @@ async def get(self, request): @log_invalid_auth async def post(self, request, data): """Create a new login flow.""" - if not indieauth.verify_redirect_uri(data['client_id'], - data['redirect_uri']): + if not await indieauth.verify_redirect_uri( + request.app['hass'], data['client_id'], data['redirect_uri']): return self.json_message('invalid client id or redirect uri', 400) if isinstance(data['handler'], list): diff --git a/tests/components/auth/test_indieauth.py b/tests/components/auth/test_indieauth.py index 7bd720ddf70033..75e61af2e715f1 100644 --- a/tests/components/auth/test_indieauth.py +++ b/tests/components/auth/test_indieauth.py @@ -1,8 +1,12 @@ """Tests for the client validator.""" -from homeassistant.components.auth import indieauth +from unittest.mock import patch import pytest +from homeassistant.components.auth import indieauth + +from tests.common import mock_coro + def test_client_id_scheme(): """Test we enforce valid scheme.""" @@ -84,27 +88,65 @@ def test_parse_url_path(): assert indieauth._parse_url('http://ex.com').path == '/' -def test_verify_redirect_uri(): +async def test_verify_redirect_uri(): """Test that we verify redirect uri correctly.""" - assert indieauth.verify_redirect_uri( + assert await indieauth.verify_redirect_uri( + None, 'http://ex.com', 'http://ex.com/callback' ) - # Different domain - assert not indieauth.verify_redirect_uri( - 'http://ex.com', - 'http://different.com/callback' - ) - - # Different scheme - assert not indieauth.verify_redirect_uri( - 'http://ex.com', - 'https://ex.com/callback' - ) - - # Different subdomain - assert not indieauth.verify_redirect_uri( - 'https://sub1.ex.com', - 'https://sub2.ex.com/callback' - ) + with patch.object(indieauth, 'fetch_redirect_uris', + side_effect=lambda *_: mock_coro([])): + # Different domain + assert not await indieauth.verify_redirect_uri( + None, + 'http://ex.com', + 'http://different.com/callback' + ) + + # Different scheme + assert not await indieauth.verify_redirect_uri( + None, + 'http://ex.com', + 'https://ex.com/callback' + ) + + # Different subdomain + assert not await indieauth.verify_redirect_uri( + None, + 'https://sub1.ex.com', + 'https://sub2.ex.com/callback' + ) + + +async def test_find_link_tag(hass, aioclient_mock): + """Test finding link tag.""" + aioclient_mock.get("http://127.0.0.1:8000", text=""" + + + + + + + + ... + +""") + redirect_uris = await indieauth.fetch_redirect_uris( + hass, "http://127.0.0.1:8000") + + assert redirect_uris == [ + "hass://oauth2_redirect", + "http://127.0.0.1:8000/beer", + ] + + +async def test_find_link_tag_max_size(hass, aioclient_mock): + """Test finding link tag.""" + text = ("0" * 1024 * 10) + '' + aioclient_mock.get("http://127.0.0.1:8000", text=text) + redirect_uris = await indieauth.fetch_redirect_uris( + hass, "http://127.0.0.1:8000") + + assert redirect_uris == [] diff --git a/tests/helpers/test_aiohttp_client.py b/tests/helpers/test_aiohttp_client.py index 28bb31c848204d..ccfe1b1aff9315 100644 --- a/tests/helpers/test_aiohttp_client.py +++ b/tests/helpers/test_aiohttp_client.py @@ -135,9 +135,8 @@ def test_get_clientsession_cleanup_without_ssl(self): @asyncio.coroutine def test_async_aiohttp_proxy_stream(aioclient_mock, camera_client): """Test that it fetches the given url.""" - aioclient_mock.get('http://example.com/mjpeg_stream', content=[ - b'Frame1', b'Frame2', b'Frame3' - ]) + aioclient_mock.get('http://example.com/mjpeg_stream', + content=b'Frame1Frame2Frame3') resp = yield from camera_client.get( '/api/camera_proxy_stream/camera.config_test') @@ -145,7 +144,7 @@ def test_async_aiohttp_proxy_stream(aioclient_mock, camera_client): assert resp.status == 200 assert aioclient_mock.call_count == 1 body = yield from resp.text() - assert body == 'Frame3Frame2Frame1' + assert body == 'Frame1Frame2Frame3' @asyncio.coroutine diff --git a/tests/test_util/aiohttp.py b/tests/test_util/aiohttp.py index 0296b8c2fbab54..813eb84707c3c3 100644 --- a/tests/test_util/aiohttp.py +++ b/tests/test_util/aiohttp.py @@ -7,6 +7,7 @@ from urllib.parse import parse_qs from aiohttp import ClientSession +from aiohttp.streams import StreamReader from yarl import URL from aiohttp.client_exceptions import ClientResponseError @@ -14,6 +15,15 @@ retype = type(re.compile('')) +def mock_stream(data): + """Mock a stream with data.""" + protocol = mock.Mock(_reading_paused=False) + stream = StreamReader(protocol) + stream.feed_data(data) + stream.feed_eof() + return stream + + class AiohttpClientMocker: """Mock Aiohttp client requests.""" @@ -45,7 +55,7 @@ def request(self, method, url, *, if not isinstance(url, retype): url = URL(url) if params: - url = url.with_query(params) + url = url.with_query(params) self._mocks.append(AiohttpClientMockResponse( method, url, status, content, cookies, exc, headers)) @@ -130,18 +140,6 @@ def __init__(self, method, url, status, response, cookies=None, exc=None, cookie.value = data self._cookies[name] = cookie - if isinstance(response, list): - self.content = mock.MagicMock() - - @asyncio.coroutine - def read(*argc, **kwargs): - """Read content stream mock.""" - if self.response: - return self.response.pop() - return None - - self.content.read = read - def match_request(self, method, url, params=None): """Test if response answers request.""" if method.lower() != self.method.lower(): @@ -177,6 +175,11 @@ def cookies(self): """Return dict of cookies.""" return self._cookies + @property + def content(self): + """Return content.""" + return mock_stream(self.response) + @asyncio.coroutine def read(self): """Return mock response.""" From 81604a9326e3ad81a436a293736d9f95d7712517 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 10 Aug 2018 19:22:12 +0200 Subject: [PATCH 086/117] deCONZ - Add support for sirens (#15896) * Add support for sirenes * Too quick... * Fix test * Use siren instead of sirene --- homeassistant/components/deconz/const.py | 4 +- homeassistant/components/switch/deconz.py | 52 +++++++++++++++++++---- tests/components/switch/test_deconz.py | 9 +++- 3 files changed, 55 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/deconz/const.py b/homeassistant/components/deconz/const.py index 7e16a9d7f107e0..e7bc5605aee400 100644 --- a/homeassistant/components/deconz/const.py +++ b/homeassistant/components/deconz/const.py @@ -15,4 +15,6 @@ ATTR_DARK = 'dark' ATTR_ON = 'on' -SWITCH_TYPES = ["On/Off plug-in unit", "Smart plug"] +POWER_PLUGS = ["On/Off plug-in unit", "Smart plug"] +SIRENS = ["Warning device"] +SWITCH_TYPES = POWER_PLUGS + SIRENS diff --git a/homeassistant/components/switch/deconz.py b/homeassistant/components/switch/deconz.py index 95e7d7367392db..d5fb22e97c467f 100644 --- a/homeassistant/components/switch/deconz.py +++ b/homeassistant/components/switch/deconz.py @@ -5,7 +5,8 @@ https://home-assistant.io/components/switch.deconz/ """ from homeassistant.components.deconz.const import ( - DOMAIN as DATA_DECONZ, DATA_DECONZ_ID, DATA_DECONZ_UNSUB, SWITCH_TYPES) + DOMAIN as DATA_DECONZ, DATA_DECONZ_ID, DATA_DECONZ_UNSUB, + POWER_PLUGS, SIRENS) from homeassistant.components.switch import SwitchDevice from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -29,8 +30,10 @@ def async_add_switch(lights): """Add switch from deCONZ.""" entities = [] for light in lights: - if light.type in SWITCH_TYPES: - entities.append(DeconzSwitch(light)) + if light.type in POWER_PLUGS: + entities.append(DeconzPowerPlug(light)) + elif light.type in SIRENS: + entities.append(DeconzSiren(light)) async_add_devices(entities, True) hass.data[DATA_DECONZ_UNSUB].append( @@ -56,11 +59,6 @@ def async_update_callback(self, reason): """Update the switch's state.""" self.async_schedule_update_ha_state() - @property - def is_on(self): - """Return true if switch is on.""" - return self._switch.state - @property def name(self): """Return the name of the switch.""" @@ -71,6 +69,25 @@ def unique_id(self): """Return a unique identifier for this switch.""" return self._switch.uniqueid + @property + def available(self): + """Return True if light is available.""" + return self._switch.reachable + + @property + def should_poll(self): + """No polling needed.""" + return False + + +class DeconzPowerPlug(DeconzSwitch): + """Representation of power plugs from deCONZ.""" + + @property + def is_on(self): + """Return true if switch is on.""" + return self._switch.state + async def async_turn_on(self, **kwargs): """Turn on switch.""" data = {'on': True} @@ -80,3 +97,22 @@ async def async_turn_off(self, **kwargs): """Turn off switch.""" data = {'on': False} await self._switch.async_set_state(data) + + +class DeconzSiren(DeconzSwitch): + """Representation of sirens from deCONZ.""" + + @property + def is_on(self): + """Return true if switch is on.""" + return self._switch.alert == 'lselect' + + async def async_turn_on(self, **kwargs): + """Turn on switch.""" + data = {'alert': 'lselect'} + await self._switch.async_set_state(data) + + async def async_turn_off(self, **kwargs): + """Turn off switch.""" + data = {'alert': 'none'} + await self._switch.async_set_state(data) diff --git a/tests/components/switch/test_deconz.py b/tests/components/switch/test_deconz.py index 490a0e67c9dd59..57fc8b3bcd9c23 100644 --- a/tests/components/switch/test_deconz.py +++ b/tests/components/switch/test_deconz.py @@ -20,6 +20,12 @@ "name": "Switch 2 name", "type": "Smart plug", "state": {} + }, + "3": { + "id": "Switch 3 id", + "name": "Switch 3 name", + "type": "Warning device", + "state": {} } } @@ -67,8 +73,9 @@ async def test_switch(hass): await setup_bridge(hass, {"lights": SUPPORTED_SWITCHES}) assert "switch.switch_1_name" in hass.data[deconz.DATA_DECONZ_ID] assert "switch.switch_2_name" in hass.data[deconz.DATA_DECONZ_ID] + assert "switch.switch_3_name" in hass.data[deconz.DATA_DECONZ_ID] assert len(SUPPORTED_SWITCHES) == len(SWITCH_TYPES) - assert len(hass.states.async_all()) == 3 + assert len(hass.states.async_all()) == 4 async def test_add_new_switch(hass): From 055e35b2975ae4a4307a4e1a71eaf6bd83cfb023 Mon Sep 17 00:00:00 2001 From: cgtobi Date: Fri, 10 Aug 2018 19:35:09 +0200 Subject: [PATCH 087/117] Add RMV public transport sensor (#15814) * Add new public transport sensor for RMV (Rhein-Main area). * Add required module. * Fix naming problem. * Add unit test. * Update dependency version to 0.0.5. * Add new requirements. * Fix variable name. * Fix issues pointed out in review. * Remove unnecessary code. * Fix linter error. * Fix config value validation. * Replace minutes as state by departure timestamp. (see ##14983) * More work on the timestamp. (see ##14983) * Revert timestamp work until #14983 gets merged. * Simplify product validation. * Remove redundant code. * Address code change requests. * Address more code change requests. * Address even more code change requests. * Simplify destination check. * Fix linter problem. * Bump dependency version to 0.0.7. * Name variable more explicit. * Only query once a minute. * Update test case. * Fix config validation. * Remove unneeded import. --- .../components/sensor/rmvtransport.py | 202 ++++++++++++++++++ requirements_all.txt | 3 + requirements_test_all.txt | 3 + script/gen_requirements_all.py | 1 + tests/components/sensor/test_rmvtransport.py | 173 +++++++++++++++ 5 files changed, 382 insertions(+) create mode 100644 homeassistant/components/sensor/rmvtransport.py create mode 100644 tests/components/sensor/test_rmvtransport.py diff --git a/homeassistant/components/sensor/rmvtransport.py b/homeassistant/components/sensor/rmvtransport.py new file mode 100644 index 00000000000000..3d7fd2aa3b70a9 --- /dev/null +++ b/homeassistant/components/sensor/rmvtransport.py @@ -0,0 +1,202 @@ +""" +Support for real-time departure information for Rhein-Main public transport. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.rmvtransport/ +""" +import logging + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import (CONF_NAME, ATTR_ATTRIBUTION) + +REQUIREMENTS = ['PyRMVtransport==0.0.7'] + +_LOGGER = logging.getLogger(__name__) + +CONF_NEXT_DEPARTURE = 'next_departure' + +CONF_STATION = 'station' +CONF_DESTINATIONS = 'destinations' +CONF_DIRECTIONS = 'directions' +CONF_LINES = 'lines' +CONF_PRODUCTS = 'products' +CONF_TIME_OFFSET = 'time_offset' +CONF_MAX_JOURNEYS = 'max_journeys' + +DEFAULT_NAME = 'RMV Journey' + +VALID_PRODUCTS = ['U-Bahn', 'Tram', 'Bus', 'S', 'RB', 'RE', 'EC', 'IC', 'ICE'] + +ICONS = { + 'U-Bahn': 'mdi:subway', + 'Tram': 'mdi:tram', + 'Bus': 'mdi:bus', + 'S': 'mdi:train', + 'RB': 'mdi:train', + 'RE': 'mdi:train', + 'EC': 'mdi:train', + 'IC': 'mdi:train', + 'ICE': 'mdi:train', + 'SEV': 'mdi:checkbox-blank-circle-outline', + None: 'mdi:clock' +} +ATTRIBUTION = "Data provided by opendata.rmv.de" + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_NEXT_DEPARTURE): [{ + vol.Required(CONF_STATION): cv.string, + vol.Optional(CONF_DESTINATIONS, default=[]): + vol.All(cv.ensure_list, [cv.string]), + vol.Optional(CONF_DIRECTIONS, default=[]): + vol.All(cv.ensure_list, [cv.string]), + vol.Optional(CONF_LINES, default=[]): + vol.All(cv.ensure_list, [cv.positive_int, cv.string]), + vol.Optional(CONF_PRODUCTS, default=VALID_PRODUCTS): + vol.All(cv.ensure_list, [vol.In(VALID_PRODUCTS)]), + vol.Optional(CONF_TIME_OFFSET, default=0): cv.positive_int, + vol.Optional(CONF_MAX_JOURNEYS, default=5): cv.positive_int, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string}] +}) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the RMV departure sensor.""" + sensors = [] + for next_departure in config.get(CONF_NEXT_DEPARTURE): + sensors.append( + RMVDepartureSensor( + next_departure[CONF_STATION], + next_departure.get(CONF_DESTINATIONS), + next_departure.get(CONF_DIRECTIONS), + next_departure.get(CONF_LINES), + next_departure.get(CONF_PRODUCTS), + next_departure.get(CONF_TIME_OFFSET), + next_departure.get(CONF_MAX_JOURNEYS), + next_departure.get(CONF_NAME))) + add_entities(sensors, True) + + +class RMVDepartureSensor(Entity): + """Implementation of an RMV departure sensor.""" + + def __init__(self, station, destinations, directions, + lines, products, time_offset, max_journeys, name): + """Initialize the sensor.""" + self._station = station + self._name = name + self._state = None + self.data = RMVDepartureData(station, destinations, directions, lines, + products, time_offset, max_journeys) + self._icon = ICONS[None] + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def available(self): + """Return True if entity is available.""" + return self._state is not None + + @property + def state(self): + """Return the next departure time.""" + return self._state + + @property + def state_attributes(self): + """Return the state attributes.""" + try: + return { + 'next_departures': [val for val in self.data.departures[1:]], + 'direction': self.data.departures[0].get('direction'), + 'line': self.data.departures[0].get('line'), + 'minutes': self.data.departures[0].get('minutes'), + 'departure_time': + self.data.departures[0].get('departure_time'), + 'product': self.data.departures[0].get('product'), + } + except IndexError: + return {} + + @property + def icon(self): + """Icon to use in the frontend, if any.""" + return self._icon + + @property + def unit_of_measurement(self): + """Return the unit this state is expressed in.""" + return "min" + + def update(self): + """Get the latest data and update the state.""" + self.data.update() + if not self.data.departures: + self._state = None + self._icon = ICONS[None] + return + if self._name == DEFAULT_NAME: + self._name = self.data.station + self._station = self.data.station + self._state = self.data.departures[0].get('minutes') + self._icon = ICONS[self.data.departures[0].get('product')] + + +class RMVDepartureData: + """Pull data from the opendata.rmv.de web page.""" + + def __init__(self, station_id, destinations, directions, + lines, products, time_offset, max_journeys): + """Initialize the sensor.""" + import RMVtransport + self.station = None + self._station_id = station_id + self._destinations = destinations + self._directions = directions + self._lines = lines + self._products = products + self._time_offset = time_offset + self._max_journeys = max_journeys + self.rmv = RMVtransport.RMVtransport() + self.departures = [] + + def update(self): + """Update the connection data.""" + try: + _data = self.rmv.get_departures(self._station_id, + products=self._products, + maxJourneys=50) + except ValueError: + self.departures = [] + _LOGGER.warning("Returned data not understood") + return + self.station = _data.get('station') + _deps = [] + for journey in _data['journeys']: + # find the first departure meeting the criteria + _nextdep = {ATTR_ATTRIBUTION: ATTRIBUTION} + if self._destinations: + dest_found = False + for dest in self._destinations: + if dest in journey['stops']: + dest_found = True + _nextdep['destination'] = dest + if not dest_found: + continue + elif self._lines and journey['number'] not in self._lines: + continue + elif journey['minutes'] < self._time_offset: + continue + for attr in ['direction', 'departure_time', 'product', 'minutes']: + _nextdep[attr] = journey.get(attr, '') + _nextdep['line'] = journey.get('number', '') + _deps.append(_nextdep) + if len(_deps) > self._max_journeys: + break + self.departures = _deps diff --git a/requirements_all.txt b/requirements_all.txt index 358225b69ea724..7732dde0f320a5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -47,6 +47,9 @@ PyMVGLive==1.1.4 # homeassistant.components.arduino PyMata==2.14 +# homeassistant.components.sensor.rmvtransport +PyRMVtransport==0.0.7 + # homeassistant.components.xiaomi_aqara PyXiaomiGateway==0.9.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a4b43f4ccc7751..f4be67c624a269 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -24,6 +24,9 @@ HAP-python==2.2.2 # homeassistant.components.notify.html5 PyJWT==1.6.0 +# homeassistant.components.sensor.rmvtransport +PyRMVtransport==0.0.7 + # homeassistant.components.sonos SoCo==0.14 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 28c96e737ff171..7652d29086b227 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -77,6 +77,7 @@ 'pymonoprice', 'pynx584', 'pyqwikswitch', + 'PyRMVtransport', 'python-forecastio', 'python-nest', 'pytradfri\[async\]', diff --git a/tests/components/sensor/test_rmvtransport.py b/tests/components/sensor/test_rmvtransport.py new file mode 100644 index 00000000000000..9db19ecde499ed --- /dev/null +++ b/tests/components/sensor/test_rmvtransport.py @@ -0,0 +1,173 @@ +"""The tests for the rmvtransport platform.""" +import unittest +from unittest.mock import patch +import datetime + +from homeassistant.setup import setup_component + +from tests.common import get_test_home_assistant + +VALID_CONFIG_MINIMAL = {'sensor': {'platform': 'rmvtransport', + 'next_departure': [{'station': '3000010'}]}} + +VALID_CONFIG_NAME = {'sensor': { + 'platform': 'rmvtransport', + 'next_departure': [ + { + 'station': '3000010', + 'name': 'My Station', + } + ]}} + +VALID_CONFIG_MISC = {'sensor': { + 'platform': 'rmvtransport', + 'next_departure': [ + { + 'station': '3000010', + 'lines': [21, 'S8'], + 'max_journeys': 2, + 'time_offset': 10 + } + ]}} + +VALID_CONFIG_DEST = {'sensor': { + 'platform': 'rmvtransport', + 'next_departure': [ + { + 'station': '3000010', + 'destinations': ['Frankfurt (Main) Flughafen Regionalbahnhof', + 'Frankfurt (Main) Stadion'] + } + ]}} + + +def get_departuresMock(stationId, maxJourneys, + products): # pylint: disable=invalid-name + """Mock rmvtransport departures loading.""" + data = {'station': 'Frankfurt (Main) Hauptbahnhof', + 'stationId': '3000010', 'filter': '11111111111', 'journeys': [ + {'product': 'Tram', 'number': 12, 'trainId': '1123456', + 'direction': 'Frankfurt (Main) Hugo-Junkers-Straße/Schleife', + 'departure_time': datetime.datetime(2018, 8, 6, 14, 21), + 'minutes': 7, 'delay': 3, 'stops': [ + 'Frankfurt (Main) Willy-Brandt-Platz', + 'Frankfurt (Main) Römer/Paulskirche', + 'Frankfurt (Main) Börneplatz', + 'Frankfurt (Main) Konstablerwache', + 'Frankfurt (Main) Bornheim Mitte', + 'Frankfurt (Main) Saalburg-/Wittelsbacherallee', + 'Frankfurt (Main) Eissporthalle/Festplatz', + 'Frankfurt (Main) Hugo-Junkers-Straße/Schleife'], + 'info': None, 'info_long': None, + 'icon': 'https://products/32_pic.png'}, + {'product': 'Bus', 'number': 21, 'trainId': '1234567', + 'direction': 'Frankfurt (Main) Hugo-Junkers-Straße/Schleife', + 'departure_time': datetime.datetime(2018, 8, 6, 14, 22), + 'minutes': 8, 'delay': 1, 'stops': [ + 'Frankfurt (Main) Weser-/Münchener Straße', + 'Frankfurt (Main) Hugo-Junkers-Straße/Schleife'], + 'info': None, 'info_long': None, + 'icon': 'https://products/32_pic.png'}, + {'product': 'Bus', 'number': 12, 'trainId': '1234568', + 'direction': 'Frankfurt (Main) Hugo-Junkers-Straße/Schleife', + 'departure_time': datetime.datetime(2018, 8, 6, 14, 25), + 'minutes': 11, 'delay': 1, 'stops': [ + 'Frankfurt (Main) Stadion'], + 'info': None, 'info_long': None, + 'icon': 'https://products/32_pic.png'}, + {'product': 'Bus', 'number': 21, 'trainId': '1234569', + 'direction': 'Frankfurt (Main) Hugo-Junkers-Straße/Schleife', + 'departure_time': datetime.datetime(2018, 8, 6, 14, 25), + 'minutes': 11, 'delay': 1, 'stops': [], + 'info': None, 'info_long': None, + 'icon': 'https://products/32_pic.png'}, + {'product': 'Bus', 'number': 12, 'trainId': '1234570', + 'direction': 'Frankfurt (Main) Hugo-Junkers-Straße/Schleife', + 'departure_time': datetime.datetime(2018, 8, 6, 14, 25), + 'minutes': 11, 'delay': 1, 'stops': [], + 'info': None, 'info_long': None, + 'icon': 'https://products/32_pic.png'}, + {'product': 'Bus', 'number': 21, 'trainId': '1234571', + 'direction': 'Frankfurt (Main) Hugo-Junkers-Straße/Schleife', + 'departure_time': datetime.datetime(2018, 8, 6, 14, 25), + 'minutes': 11, 'delay': 1, 'stops': [], + 'info': None, 'info_long': None, + 'icon': 'https://products/32_pic.png'} + ]} + return data + + +def get_errDeparturesMock(stationId, maxJourneys, + products): # pylint: disable=invalid-name + """Mock rmvtransport departures erroneous loading.""" + raise ValueError + + +class TestRMVtransportSensor(unittest.TestCase): + """Test the rmvtransport sensor.""" + + def setUp(self): + """Set up things to run when tests begin.""" + self.hass = get_test_home_assistant() + self.config = VALID_CONFIG_MINIMAL + self.reference = {} + self.entities = [] + + def tearDown(self): + """Stop everything that was started.""" + self.hass.stop() + + @patch('RMVtransport.RMVtransport.get_departures', + side_effect=get_departuresMock) + def test_rmvtransport_min_config(self, mock_get_departures): + """Test minimal rmvtransport configuration.""" + assert setup_component(self.hass, 'sensor', VALID_CONFIG_MINIMAL) + state = self.hass.states.get('sensor.frankfurt_main_hauptbahnhof') + self.assertEqual(state.state, '7') + self.assertEqual(state.attributes['departure_time'], + datetime.datetime(2018, 8, 6, 14, 21)) + self.assertEqual(state.attributes['direction'], + 'Frankfurt (Main) Hugo-Junkers-Straße/Schleife') + self.assertEqual(state.attributes['product'], 'Tram') + self.assertEqual(state.attributes['line'], 12) + self.assertEqual(state.attributes['icon'], 'mdi:tram') + self.assertEqual(state.attributes['friendly_name'], + 'Frankfurt (Main) Hauptbahnhof') + + @patch('RMVtransport.RMVtransport.get_departures', + side_effect=get_departuresMock) + def test_rmvtransport_name_config(self, mock_get_departures): + """Test custom name configuration.""" + assert setup_component(self.hass, 'sensor', VALID_CONFIG_NAME) + state = self.hass.states.get('sensor.my_station') + self.assertEqual(state.attributes['friendly_name'], 'My Station') + + @patch('RMVtransport.RMVtransport.get_departures', + side_effect=get_errDeparturesMock) + def test_rmvtransport_err_config(self, mock_get_departures): + """Test erroneous rmvtransport configuration.""" + assert setup_component(self.hass, 'sensor', VALID_CONFIG_MINIMAL) + + @patch('RMVtransport.RMVtransport.get_departures', + side_effect=get_departuresMock) + def test_rmvtransport_misc_config(self, mock_get_departures): + """Test misc configuration.""" + assert setup_component(self.hass, 'sensor', VALID_CONFIG_MISC) + state = self.hass.states.get('sensor.frankfurt_main_hauptbahnhof') + self.assertEqual(state.attributes['friendly_name'], + 'Frankfurt (Main) Hauptbahnhof') + self.assertEqual(state.attributes['line'], 21) + + @patch('RMVtransport.RMVtransport.get_departures', + side_effect=get_departuresMock) + def test_rmvtransport_dest_config(self, mock_get_departures): + """Test misc configuration.""" + assert setup_component(self.hass, 'sensor', VALID_CONFIG_DEST) + state = self.hass.states.get('sensor.frankfurt_main_hauptbahnhof') + self.assertEqual(state.state, '11') + self.assertEqual(state.attributes['direction'], + 'Frankfurt (Main) Hugo-Junkers-Straße/Schleife') + self.assertEqual(state.attributes['line'], 12) + self.assertEqual(state.attributes['minutes'], 11) + self.assertEqual(state.attributes['departure_time'], + datetime.datetime(2018, 8, 6, 14, 25)) From e17e080639e93e6d02c3345cb05d7aa1cf4bd7d5 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 11 Aug 2018 08:47:41 +0200 Subject: [PATCH 088/117] :pencil2: Corrects typo in code comments (#15923) `MomematicIP` -> `HomematicIP` --- homeassistant/components/binary_sensor/homematicip_cloud.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/binary_sensor/homematicip_cloud.py b/homeassistant/components/binary_sensor/homematicip_cloud.py index 1ab4fe74d695da..962817827f0ac6 100644 --- a/homeassistant/components/binary_sensor/homematicip_cloud.py +++ b/homeassistant/components/binary_sensor/homematicip_cloud.py @@ -65,7 +65,7 @@ def is_on(self): class HomematicipMotionDetector(HomematicipGenericDevice, BinarySensorDevice): - """MomematicIP motion detector.""" + """HomematicIP motion detector.""" @property def device_class(self): @@ -81,7 +81,7 @@ def is_on(self): class HomematicipSmokeDetector(HomematicipGenericDevice, BinarySensorDevice): - """MomematicIP smoke detector.""" + """HomematicIP smoke detector.""" @property def device_class(self): From f24773933c0826d1091a27c6e9587f987a1e3466 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 11 Aug 2018 08:58:20 +0200 Subject: [PATCH 089/117] Update frontend to 20180811.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 0dcf7526262d5d..e248bc20ccd3b0 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -26,7 +26,7 @@ from homeassistant.loader import bind_hass from homeassistant.util.yaml import load_yaml -REQUIREMENTS = ['home-assistant-frontend==20180804.0'] +REQUIREMENTS = ['home-assistant-frontend==20180811.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log', diff --git a/requirements_all.txt b/requirements_all.txt index 7732dde0f320a5..7f4521e352253e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -433,7 +433,7 @@ hole==0.3.0 holidays==0.9.6 # homeassistant.components.frontend -home-assistant-frontend==20180804.0 +home-assistant-frontend==20180811.0 # homeassistant.components.homekit_controller # homekit==0.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f4be67c624a269..cfe98bd7d4e5f1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -84,7 +84,7 @@ hbmqtt==0.9.2 holidays==0.9.6 # homeassistant.components.frontend -home-assistant-frontend==20180804.0 +home-assistant-frontend==20180811.0 # homeassistant.components.homematicip_cloud homematicip==0.9.8 From 69934a9598c3fcd56153852e4d894734d5691ae1 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 11 Aug 2018 08:58:52 +0200 Subject: [PATCH 090/117] Bumped version to 0.76.0b0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 1b9dc8986a5902..65505bf30ec2fa 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 76 -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 e0229b799d59e37f9f29c2600fb37e8046b38678 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 13 Aug 2018 21:27:34 +0200 Subject: [PATCH 091/117] Update frontend to 20180813.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 e248bc20ccd3b0..41cfdd3edd80ce 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -26,7 +26,7 @@ from homeassistant.loader import bind_hass from homeassistant.util.yaml import load_yaml -REQUIREMENTS = ['home-assistant-frontend==20180811.0'] +REQUIREMENTS = ['home-assistant-frontend==20180813.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log', diff --git a/requirements_all.txt b/requirements_all.txt index 7f4521e352253e..2f31b4359bd07d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -433,7 +433,7 @@ hole==0.3.0 holidays==0.9.6 # homeassistant.components.frontend -home-assistant-frontend==20180811.0 +home-assistant-frontend==20180813.0 # homeassistant.components.homekit_controller # homekit==0.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cfe98bd7d4e5f1..ffc55c23210236 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -84,7 +84,7 @@ hbmqtt==0.9.2 holidays==0.9.6 # homeassistant.components.frontend -home-assistant-frontend==20180811.0 +home-assistant-frontend==20180813.0 # homeassistant.components.homematicip_cloud homematicip==0.9.8 From 985f96662e3a8cc3783e647268f8ee2ede5eb5d2 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Sun, 12 Aug 2018 20:22:54 +0200 Subject: [PATCH 092/117] Upgrade pymysensors to 0.17.0 (#15942) --- homeassistant/components/mysensors/__init__.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/mysensors/__init__.py b/homeassistant/components/mysensors/__init__.py index 980efcf5805d9f..e498539f2f9e78 100644 --- a/homeassistant/components/mysensors/__init__.py +++ b/homeassistant/components/mysensors/__init__.py @@ -22,7 +22,7 @@ from .device import get_mysensors_devices from .gateway import get_mysensors_gateway, setup_gateways, finish_setup -REQUIREMENTS = ['pymysensors==0.16.0'] +REQUIREMENTS = ['pymysensors==0.17.0'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 2f31b4359bd07d..5a2235df870ccb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -953,7 +953,7 @@ pymusiccast==0.1.6 pymyq==0.0.11 # homeassistant.components.mysensors -pymysensors==0.16.0 +pymysensors==0.17.0 # homeassistant.components.lock.nello pynello==1.5.1 From c0830f1c20d27f770a833f5fa4c99d1066cdf908 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 13 Aug 2018 22:39:13 +0200 Subject: [PATCH 093/117] Deprecate remote.api (#15955) --- homeassistant/remote.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/remote.py b/homeassistant/remote.py index 313f98a890c741..c254dd500f77ea 100644 --- a/homeassistant/remote.py +++ b/homeassistant/remote.py @@ -48,6 +48,7 @@ def __init__(self, host: str, api_password: Optional[str] = None, port: Optional[int] = SERVER_PORT, use_ssl: bool = False) -> None: """Init the API.""" + _LOGGER.warning('This class is deprecated and will be removed in 0.77') self.host = host self.port = port self.api_password = api_password From 9e217651730d2baac909aa6ca6112060f1ebcb64 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 13 Aug 2018 23:17:30 +0200 Subject: [PATCH 094/117] Bumped version to 0.76.0b1 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 65505bf30ec2fa..52175c2b4e9203 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 76 -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 6d432d19fe751f533c7fb1341f9cfee7d32925ff Mon Sep 17 00:00:00 2001 From: kbickar Date: Tue, 14 Aug 2018 09:50:44 -0400 Subject: [PATCH 095/117] Added error handling for sense API timeouts (#15789) * Added error handling for sense API timeouts * Moved imports in function * Moved imports to more appropriate function * Change exception to custom package version --- homeassistant/components/sensor/sense.py | 9 +++++++-- requirements_all.txt | 2 +- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sensor/sense.py b/homeassistant/components/sensor/sense.py index 16f4ccb9b6c1b3..89e0d15bf488e7 100644 --- a/homeassistant/components/sensor/sense.py +++ b/homeassistant/components/sensor/sense.py @@ -16,7 +16,7 @@ from homeassistant.util import Throttle import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['sense_energy==0.3.1'] +REQUIREMENTS = ['sense_energy==0.4.1'] _LOGGER = logging.getLogger(__name__) @@ -139,7 +139,12 @@ def icon(self): def update(self): """Get the latest data, update state.""" - self.update_sensor() + from sense_energy import SenseAPITimeoutException + try: + self.update_sensor() + except SenseAPITimeoutException: + _LOGGER.error("Timeout retrieving data") + return if self._sensor_type == ACTIVE_TYPE: if self._is_production: diff --git a/requirements_all.txt b/requirements_all.txt index 5a2235df870ccb..0e6d7e1ac0727c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1265,7 +1265,7 @@ sendgrid==5.4.1 sense-hat==2.2.0 # homeassistant.components.sensor.sense -sense_energy==0.3.1 +sense_energy==0.4.1 # homeassistant.components.media_player.aquostv sharp_aquos_rc==0.3.2 From 34e1f1b6da4d748976dbd535297d1b3ef56ec176 Mon Sep 17 00:00:00 2001 From: Jason Hu Date: Mon, 13 Aug 2018 02:27:18 -0700 Subject: [PATCH 096/117] Add context to login flow (#15914) * Add context to login flow * source -> context * Fix unit test * Update comment --- homeassistant/auth/__init__.py | 4 ++-- homeassistant/auth/providers/__init__.py | 2 +- homeassistant/auth/providers/homeassistant.py | 2 +- .../auth/providers/insecure_example.py | 2 +- .../auth/providers/legacy_api_password.py | 2 +- homeassistant/components/auth/login_flow.py | 3 +-- .../components/config/config_entries.py | 2 +- homeassistant/config_entries.py | 17 +++++------------ homeassistant/data_entry_flow.py | 8 ++++---- tests/components/cast/test_init.py | 5 +++-- tests/components/config/test_config_entries.py | 4 +--- tests/components/sonos/test_init.py | 5 +++-- tests/helpers/test_config_entry_flow.py | 3 ++- tests/test_config_entries.py | 9 ++++++--- tests/test_data_entry_flow.py | 6 ++++-- 15 files changed, 36 insertions(+), 38 deletions(-) diff --git a/homeassistant/auth/__init__.py b/homeassistant/auth/__init__.py index 8eaa9cdbb97b4e..9695e77f6f1098 100644 --- a/homeassistant/auth/__init__.py +++ b/homeassistant/auth/__init__.py @@ -215,9 +215,9 @@ async def _async_create_login_flow(self, handler, *, context, data): """Create a login flow.""" auth_provider = self._providers[handler] - return await auth_provider.async_credential_flow() + return await auth_provider.async_credential_flow(context) - async def _async_finish_login_flow(self, result): + async def _async_finish_login_flow(self, context, result): """Result of a credential login flow.""" if result['type'] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY: return None diff --git a/homeassistant/auth/providers/__init__.py b/homeassistant/auth/providers/__init__.py index 68cc1c7edd2722..ac5b6107b8ac99 100644 --- a/homeassistant/auth/providers/__init__.py +++ b/homeassistant/auth/providers/__init__.py @@ -123,7 +123,7 @@ def async_create_credentials(self, data): # Implement by extending class - async def async_credential_flow(self): + async def async_credential_flow(self, context): """Return the data flow for logging in with auth provider.""" raise NotImplementedError diff --git a/homeassistant/auth/providers/homeassistant.py b/homeassistant/auth/providers/homeassistant.py index e9693b09634bcc..5a2355264ab8ae 100644 --- a/homeassistant/auth/providers/homeassistant.py +++ b/homeassistant/auth/providers/homeassistant.py @@ -158,7 +158,7 @@ async def async_initialize(self): self.data = Data(self.hass) await self.data.async_load() - async def async_credential_flow(self): + async def async_credential_flow(self, context): """Return a flow to login.""" return LoginFlow(self) diff --git a/homeassistant/auth/providers/insecure_example.py b/homeassistant/auth/providers/insecure_example.py index c86c8eb71f1124..96f824140ed877 100644 --- a/homeassistant/auth/providers/insecure_example.py +++ b/homeassistant/auth/providers/insecure_example.py @@ -31,7 +31,7 @@ class InvalidAuthError(HomeAssistantError): class ExampleAuthProvider(AuthProvider): """Example auth provider based on hardcoded usernames and passwords.""" - async def async_credential_flow(self): + async def async_credential_flow(self, context): """Return a flow to login.""" return LoginFlow(self) diff --git a/homeassistant/auth/providers/legacy_api_password.py b/homeassistant/auth/providers/legacy_api_password.py index 1f92fb60f13752..f2f467e07ec1f0 100644 --- a/homeassistant/auth/providers/legacy_api_password.py +++ b/homeassistant/auth/providers/legacy_api_password.py @@ -36,7 +36,7 @@ class LegacyApiPasswordAuthProvider(AuthProvider): DEFAULT_TITLE = 'Legacy API Password' - async def async_credential_flow(self): + async def async_credential_flow(self, context): """Return a flow to login.""" return LoginFlow(self) diff --git a/homeassistant/components/auth/login_flow.py b/homeassistant/components/auth/login_flow.py index 8b983b6d19f2b4..7b80e52a8d712d 100644 --- a/homeassistant/components/auth/login_flow.py +++ b/homeassistant/components/auth/login_flow.py @@ -54,7 +54,6 @@ "flow_id": "8f7e42faab604bcab7ac43c44ca34d58", "handler": ["insecure_example", null], "result": "411ee2f916e648d691e937ae9344681e", - "source": "user", "title": "Example", "type": "create_entry", "version": 1 @@ -152,7 +151,7 @@ async def post(self, request, data): handler = data['handler'] try: - result = await self._flow_mgr.async_init(handler) + result = await self._flow_mgr.async_init(handler, context={}) except data_entry_flow.UnknownHandler: return self.json_message('Invalid handler specified', 404) except data_entry_flow.UnknownStep: diff --git a/homeassistant/components/config/config_entries.py b/homeassistant/components/config/config_entries.py index 57fdbd31d20010..04d2c713cdcffc 100644 --- a/homeassistant/components/config/config_entries.py +++ b/homeassistant/components/config/config_entries.py @@ -96,7 +96,7 @@ def get(self, request): return self.json([ flw for flw in hass.config_entries.flow.async_progress() - if flw['source'] != config_entries.SOURCE_USER]) + if flw['context']['source'] != config_entries.SOURCE_USER]) class ConfigManagerFlowResourceView(FlowManagerResourceView): diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 51114a2a416917..b2e8389e4494e2 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -372,10 +372,10 @@ async def async_forward_entry_unload(self, entry, component): return await entry.async_unload( self.hass, component=getattr(self.hass.components, component)) - async def _async_finish_flow(self, result): + async def _async_finish_flow(self, context, result): """Finish a config flow and add an entry.""" # If no discovery config entries in progress, remove notification. - if not any(ent['source'] in DISCOVERY_SOURCES for ent + if not any(ent['context']['source'] in DISCOVERY_SOURCES for ent in self.hass.config_entries.flow.async_progress()): self.hass.components.persistent_notification.async_dismiss( DISCOVERY_NOTIFICATION_ID) @@ -383,15 +383,12 @@ async def _async_finish_flow(self, result): if result['type'] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY: return None - source = result['source'] - if source is None: - source = SOURCE_USER entry = ConfigEntry( version=result['version'], domain=result['handler'], title=result['title'], data=result['data'], - source=source, + source=context['source'], ) self._entries.append(entry) await self._async_schedule_save() @@ -406,7 +403,7 @@ async def _async_finish_flow(self, result): self.hass, entry.domain, self._hass_config) # Return Entry if they not from a discovery request - if result['source'] not in DISCOVERY_SOURCES: + if context['source'] not in DISCOVERY_SOURCES: return entry return entry @@ -422,10 +419,7 @@ async def _async_create_flow(self, handler_key, *, context, data): if handler is None: raise data_entry_flow.UnknownHandler - if context is not None: - source = context.get('source', SOURCE_USER) - else: - source = SOURCE_USER + source = context['source'] # Make sure requirements and dependencies of component are resolved await async_process_deps_reqs( @@ -442,7 +436,6 @@ async def _async_create_flow(self, handler_key, *, context, data): ) flow = handler() - flow.source = source flow.init_step = source return flow diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index 7609ffa615a357..f820911e39680c 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -46,7 +46,7 @@ def async_progress(self) -> List[Dict]: return [{ 'flow_id': flow.flow_id, 'handler': flow.handler, - 'source': flow.source, + 'context': flow.context, } for flow in self._progress.values()] async def async_init(self, handler: Hashable, *, context: Dict = None, @@ -57,6 +57,7 @@ async def async_init(self, handler: Hashable, *, context: Dict = None, flow.hass = self.hass flow.handler = handler flow.flow_id = uuid.uuid4().hex + flow.context = context self._progress[flow.flow_id] = flow return await self._async_handle_step(flow, flow.init_step, data) @@ -108,7 +109,7 @@ async def _async_handle_step(self, flow: Any, step_id: str, self._progress.pop(flow.flow_id) # We pass a copy of the result because we're mutating our version - entry = await self._async_finish_flow(dict(result)) + entry = await self._async_finish_flow(flow.context, dict(result)) if result['type'] == RESULT_TYPE_CREATE_ENTRY: result['result'] = entry @@ -122,8 +123,8 @@ class FlowHandler: flow_id = None hass = None handler = None - source = None cur_step = None + context = None # Set by _async_create_flow callback init_step = 'init' @@ -156,7 +157,6 @@ def async_create_entry(self, *, title: str, data: Dict) -> Dict: 'handler': self.handler, 'title': title, 'data': data, - 'source': self.source, } @callback diff --git a/tests/components/cast/test_init.py b/tests/components/cast/test_init.py index 3ed9ea7b88e11b..1ffbd375b753a7 100644 --- a/tests/components/cast/test_init.py +++ b/tests/components/cast/test_init.py @@ -1,7 +1,7 @@ """Tests for the Cast config flow.""" from unittest.mock import patch -from homeassistant import data_entry_flow +from homeassistant import config_entries, data_entry_flow from homeassistant.setup import async_setup_component from homeassistant.components import cast @@ -15,7 +15,8 @@ async def test_creating_entry_sets_up_media_player(hass): MockDependency('pychromecast', 'discovery'), \ patch('pychromecast.discovery.discover_chromecasts', return_value=True): - result = await hass.config_entries.flow.async_init(cast.DOMAIN) + result = await hass.config_entries.flow.async_init( + cast.DOMAIN, context={'source': config_entries.SOURCE_USER}) assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY await hass.async_block_till_done() diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index f85d7df1a86517..ba053050f997fb 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -202,7 +202,6 @@ def async_step_user(self, user_input=None): 'handler': 'test', 'title': 'Test Entry', 'type': 'create_entry', - 'source': 'user', 'version': 1, } @@ -264,7 +263,6 @@ def async_step_account(self, user_input=None): 'type': 'create_entry', 'title': 'user-title', 'version': 1, - 'source': 'user', } @@ -295,7 +293,7 @@ def async_step_account(self, user_input=None): { 'flow_id': form['flow_id'], 'handler': 'test', - 'source': 'hassio' + 'context': {'source': 'hassio'} } ] diff --git a/tests/components/sonos/test_init.py b/tests/components/sonos/test_init.py index 9fe22fc7e79ee5..ab4eed31fee702 100644 --- a/tests/components/sonos/test_init.py +++ b/tests/components/sonos/test_init.py @@ -1,7 +1,7 @@ """Tests for the Sonos config flow.""" from unittest.mock import patch -from homeassistant import data_entry_flow +from homeassistant import config_entries, data_entry_flow from homeassistant.setup import async_setup_component from homeassistant.components import sonos @@ -13,7 +13,8 @@ async def test_creating_entry_sets_up_media_player(hass): with patch('homeassistant.components.media_player.sonos.async_setup_entry', return_value=mock_coro(True)) as mock_setup, \ patch('soco.discover', return_value=True): - result = await hass.config_entries.flow.async_init(sonos.DOMAIN) + result = await hass.config_entries.flow.async_init( + sonos.DOMAIN, context={'source': config_entries.SOURCE_USER}) assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY await hass.async_block_till_done() diff --git a/tests/helpers/test_config_entry_flow.py b/tests/helpers/test_config_entry_flow.py index 46c58320d504b2..9eede7dff9b8d6 100644 --- a/tests/helpers/test_config_entry_flow.py +++ b/tests/helpers/test_config_entry_flow.py @@ -109,7 +109,8 @@ async def test_user_init_trumps_discovery(hass, flow_conf): assert result['type'] == data_entry_flow.RESULT_TYPE_FORM # User starts flow - result = await hass.config_entries.flow.async_init('test', data={}) + result = await hass.config_entries.flow.async_init( + 'test', context={'source': config_entries.SOURCE_USER}, data={}) assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY # Discovery flow has been aborted diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 8ac4c642b0a83d..1f6fd8756e6521 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -116,7 +116,8 @@ def async_step_user(self, user_input=None): }) with patch.dict(config_entries.HANDLERS, {'comp': TestFlow, 'beer': 5}): - yield from manager.flow.async_init('comp') + yield from manager.flow.async_init( + 'comp', context={'source': config_entries.SOURCE_USER}) yield from hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 1 @@ -171,7 +172,8 @@ def async_step_user(self, user_input=None): ) with patch.dict(config_entries.HANDLERS, {'test': TestFlow}): - await hass.config_entries.flow.async_init('test') + await hass.config_entries.flow.async_init( + 'test', context={'source': config_entries.SOURCE_USER}) class Test2Flow(data_entry_flow.FlowHandler): VERSION = 3 @@ -187,7 +189,8 @@ def async_step_user(self, user_input=None): with patch('homeassistant.config_entries.HANDLERS.get', return_value=Test2Flow): - await hass.config_entries.flow.async_init('test') + await hass.config_entries.flow.async_init( + 'test', context={'source': config_entries.SOURCE_USER}) # To trigger the call_later async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=1)) diff --git a/tests/test_data_entry_flow.py b/tests/test_data_entry_flow.py index dc10f3d8d1ae59..c5d5bbb50bfa03 100644 --- a/tests/test_data_entry_flow.py +++ b/tests/test_data_entry_flow.py @@ -25,8 +25,10 @@ async def async_create_flow(handler_name, *, context, data): if context is not None else 'user_input' return flow - async def async_add_entry(result): + async def async_add_entry(context, result): if (result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY): + result['source'] = context.get('source') \ + if context is not None else 'user' entries.append(result) manager = data_entry_flow.FlowManager( @@ -168,7 +170,7 @@ async def async_step_init(self, user_input=None): assert entry['handler'] == 'test' assert entry['title'] == 'Test Title' assert entry['data'] == 'Test Data' - assert entry['source'] == 'user_input' + assert entry['source'] == 'user' async def test_discovery_init_flow(manager): From d0e4c95bbc3a95aa4db17f504db454c86403bdc2 Mon Sep 17 00:00:00 2001 From: Jason Hu Date: Mon, 13 Aug 2018 02:26:06 -0700 Subject: [PATCH 097/117] MQTT embedded broker has to set its own password. (#15929) --- homeassistant/components/mqtt/__init__.py | 16 +++++- homeassistant/components/mqtt/server.py | 26 ++++----- tests/components/mqtt/test_server.py | 65 +++++++++++++++++++---- 3 files changed, 82 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 3928eb945aa105..70d4d7aa5d74d4 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -32,7 +32,8 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_STOP, CONF_VALUE_TEMPLATE, CONF_USERNAME, CONF_PASSWORD, CONF_PORT, CONF_PROTOCOL, CONF_PAYLOAD) -from homeassistant.components.mqtt.server import HBMQTT_CONFIG_SCHEMA + +from .server import HBMQTT_CONFIG_SCHEMA REQUIREMENTS = ['paho-mqtt==1.3.1'] @@ -306,7 +307,8 @@ async def _async_setup_server(hass: HomeAssistantType, return None success, broker_config = \ - await server.async_start(hass, conf.get(CONF_EMBEDDED)) + await server.async_start( + hass, conf.get(CONF_PASSWORD), conf.get(CONF_EMBEDDED)) if not success: return None @@ -349,6 +351,16 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: if CONF_EMBEDDED not in conf and CONF_BROKER in conf: broker_config = None else: + if (conf.get(CONF_PASSWORD) is None and + config.get('http') is not None and + config['http'].get('api_password') is not None): + _LOGGER.error("Starting from 0.77, embedded MQTT broker doesn't" + " use api_password as default password any more." + " Please set password configuration. See https://" + "home-assistant.io/docs/mqtt/broker#embedded-broker" + " for details") + return False + broker_config = await _async_setup_server(hass, config) if CONF_BROKER in conf: diff --git a/homeassistant/components/mqtt/server.py b/homeassistant/components/mqtt/server.py index 8a012928792588..5fc365342aec6b 100644 --- a/homeassistant/components/mqtt/server.py +++ b/homeassistant/components/mqtt/server.py @@ -27,27 +27,29 @@ }) }, extra=vol.ALLOW_EXTRA)) +_LOGGER = logging.getLogger(__name__) + @asyncio.coroutine -def async_start(hass, server_config): +def async_start(hass, password, server_config): """Initialize MQTT Server. This method is a coroutine. """ from hbmqtt.broker import Broker, BrokerException + passwd = tempfile.NamedTemporaryFile() try: - passwd = tempfile.NamedTemporaryFile() - if server_config is None: - server_config, client_config = generate_config(hass, passwd) + server_config, client_config = generate_config( + hass, passwd, password) else: client_config = None broker = Broker(server_config, hass.loop) yield from broker.start() except BrokerException: - logging.getLogger(__name__).exception("Error initializing MQTT server") + _LOGGER.exception("Error initializing MQTT server") return False, None finally: passwd.close() @@ -63,9 +65,10 @@ def async_shutdown_mqtt_server(event): return True, client_config -def generate_config(hass, passwd): +def generate_config(hass, passwd, password): """Generate a configuration based on current Home Assistant instance.""" - from homeassistant.components.mqtt import PROTOCOL_311 + from . import PROTOCOL_311 + config = { 'listeners': { 'default': { @@ -79,29 +82,26 @@ def generate_config(hass, passwd): }, }, 'auth': { - 'allow-anonymous': hass.config.api.api_password is None + 'allow-anonymous': password is None }, 'plugins': ['auth_anonymous'], } - if hass.config.api.api_password: + if password: username = 'homeassistant' - password = hass.config.api.api_password # Encrypt with what hbmqtt uses to verify from passlib.apps import custom_app_context passwd.write( 'homeassistant:{}\n'.format( - custom_app_context.encrypt( - hass.config.api.api_password)).encode('utf-8')) + custom_app_context.encrypt(password)).encode('utf-8')) passwd.flush() config['auth']['password-file'] = passwd.name config['plugins'].append('auth_file') else: username = None - password = None client_config = ('localhost', 1883, username, password, None, PROTOCOL_311) diff --git a/tests/components/mqtt/test_server.py b/tests/components/mqtt/test_server.py index 1c37c9049f3188..d5d54f457d683e 100644 --- a/tests/components/mqtt/test_server.py +++ b/tests/components/mqtt/test_server.py @@ -4,6 +4,7 @@ import pytest +from homeassistant.const import CONF_PASSWORD from homeassistant.setup import setup_component import homeassistant.components.mqtt as mqtt @@ -19,9 +20,6 @@ class TestMQTT: def setup_method(self, method): """Setup things to be run when tests are started.""" self.hass = get_test_home_assistant() - setup_component(self.hass, 'http', { - 'api_password': 'super_secret' - }) def teardown_method(self, method): """Stop everything that was started.""" @@ -32,14 +30,61 @@ def teardown_method(self, method): @patch('hbmqtt.broker.Broker', Mock(return_value=MagicMock())) @patch('hbmqtt.broker.Broker.start', Mock(return_value=mock_coro())) @patch('homeassistant.components.mqtt.MQTT') - def test_creating_config_with_http_pass(self, mock_mqtt): - """Test if the MQTT server gets started and subscribe/publish msg.""" + def test_creating_config_with_http_pass_only(self, mock_mqtt): + """Test if the MQTT server failed starts. + + Since 0.77, MQTT server has to setup its own password. + If user has api_password but don't have mqtt.password, MQTT component + will fail to start + """ mock_mqtt().async_connect.return_value = mock_coro(True) self.hass.bus.listen_once = MagicMock() - password = 'super_secret' + assert not setup_component(self.hass, mqtt.DOMAIN, { + 'http': {'api_password': 'http_secret'} + }) - self.hass.config.api = MagicMock(api_password=password) - assert setup_component(self.hass, mqtt.DOMAIN, {}) + @patch('passlib.apps.custom_app_context', Mock(return_value='')) + @patch('tempfile.NamedTemporaryFile', Mock(return_value=MagicMock())) + @patch('hbmqtt.broker.Broker', Mock(return_value=MagicMock())) + @patch('hbmqtt.broker.Broker.start', Mock(return_value=mock_coro())) + @patch('homeassistant.components.mqtt.MQTT') + def test_creating_config_with_pass_and_no_http_pass(self, mock_mqtt): + """Test if the MQTT server gets started with password. + + Since 0.77, MQTT server has to setup its own password. + """ + mock_mqtt().async_connect.return_value = mock_coro(True) + self.hass.bus.listen_once = MagicMock() + password = 'mqtt_secret' + + assert setup_component(self.hass, mqtt.DOMAIN, { + mqtt.DOMAIN: {CONF_PASSWORD: password}, + }) + assert mock_mqtt.called + from pprint import pprint + pprint(mock_mqtt.mock_calls) + assert mock_mqtt.mock_calls[1][1][5] == 'homeassistant' + assert mock_mqtt.mock_calls[1][1][6] == password + + @patch('passlib.apps.custom_app_context', Mock(return_value='')) + @patch('tempfile.NamedTemporaryFile', Mock(return_value=MagicMock())) + @patch('hbmqtt.broker.Broker', Mock(return_value=MagicMock())) + @patch('hbmqtt.broker.Broker.start', Mock(return_value=mock_coro())) + @patch('homeassistant.components.mqtt.MQTT') + def test_creating_config_with_pass_and_http_pass(self, mock_mqtt): + """Test if the MQTT server gets started with password. + + Since 0.77, MQTT server has to setup its own password. + """ + mock_mqtt().async_connect.return_value = mock_coro(True) + self.hass.bus.listen_once = MagicMock() + password = 'mqtt_secret' + + self.hass.config.api = MagicMock(api_password='api_password') + assert setup_component(self.hass, mqtt.DOMAIN, { + 'http': {'api_password': 'http_secret'}, + mqtt.DOMAIN: {CONF_PASSWORD: password}, + }) assert mock_mqtt.called from pprint import pprint pprint(mock_mqtt.mock_calls) @@ -51,8 +96,8 @@ def test_creating_config_with_http_pass(self, mock_mqtt): @patch('hbmqtt.broker.Broker', Mock(return_value=MagicMock())) @patch('hbmqtt.broker.Broker.start', Mock(return_value=mock_coro())) @patch('homeassistant.components.mqtt.MQTT') - def test_creating_config_with_http_no_pass(self, mock_mqtt): - """Test if the MQTT server gets started and subscribe/publish msg.""" + def test_creating_config_without_pass(self, mock_mqtt): + """Test if the MQTT server gets started without password.""" mock_mqtt().async_connect.return_value = mock_coro(True) self.hass.bus.listen_once = MagicMock() From d393380122d0092a7b2b127e7722dbd8e27972af Mon Sep 17 00:00:00 2001 From: Khalid Date: Tue, 14 Aug 2018 12:55:40 +0300 Subject: [PATCH 098/117] Fix issue when reading worxlandroid pin code (#15930) Fixes #14050 --- homeassistant/components/sensor/worxlandroid.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/worxlandroid.py b/homeassistant/components/sensor/worxlandroid.py index c49ce36bd49933..8963bb135e029f 100644 --- a/homeassistant/components/sensor/worxlandroid.py +++ b/homeassistant/components/sensor/worxlandroid.py @@ -28,7 +28,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_HOST): cv.string, vol.Required(CONF_PIN): - vol.All(vol.Coerce(int), vol.Range(min=1000, max=9999)), + vol.All(vol.Coerce(str), vol.Match(r'\d{4}')), vol.Optional(CONF_ALLOW_UNREACHABLE, default=True): cv.boolean, vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, }) From f4e84fbf84f1526b5e4a0a47a16a51797eb9961d Mon Sep 17 00:00:00 2001 From: Daniel Bowman Date: Tue, 14 Aug 2018 10:53:08 +0100 Subject: [PATCH 099/117] remove-phantomjs-from-docker (#15936) --- Dockerfile | 1 - virtualization/Docker/Dockerfile.dev | 1 - virtualization/Docker/scripts/phantomjs | 15 --------------- virtualization/Docker/setup_docker_prereqs | 5 ----- 4 files changed, 22 deletions(-) delete mode 100755 virtualization/Docker/scripts/phantomjs diff --git a/Dockerfile b/Dockerfile index 75d9e9eb716247..c84e6162d04d93 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,7 +10,6 @@ LABEL maintainer="Paulus Schoutsen " #ENV INSTALL_OPENALPR no #ENV INSTALL_FFMPEG no #ENV INSTALL_LIBCEC no -#ENV INSTALL_PHANTOMJS no #ENV INSTALL_SSOCR no #ENV INSTALL_IPERF3 no diff --git a/virtualization/Docker/Dockerfile.dev b/virtualization/Docker/Dockerfile.dev index d0599c2e74c3a0..790727030314dc 100644 --- a/virtualization/Docker/Dockerfile.dev +++ b/virtualization/Docker/Dockerfile.dev @@ -10,7 +10,6 @@ LABEL maintainer="Paulus Schoutsen " #ENV INSTALL_OPENALPR no #ENV INSTALL_FFMPEG no #ENV INSTALL_LIBCEC no -#ENV INSTALL_PHANTOMJS no #ENV INSTALL_COAP no #ENV INSTALL_SSOCR no #ENV INSTALL_IPERF3 no diff --git a/virtualization/Docker/scripts/phantomjs b/virtualization/Docker/scripts/phantomjs deleted file mode 100755 index 7700b08f293538..00000000000000 --- a/virtualization/Docker/scripts/phantomjs +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/bash -# Sets up phantomjs. - -# Stop on errors -set -e - -PHANTOMJS_VERSION="2.1.1" - -cd /usr/src/app/ -mkdir -p build && cd build - -curl -LSO https://bitbucket.org/ariya/phantomjs/downloads/phantomjs-$PHANTOMJS_VERSION-linux-x86_64.tar.bz2 -tar -xjf phantomjs-$PHANTOMJS_VERSION-linux-x86_64.tar.bz2 -mv phantomjs-$PHANTOMJS_VERSION-linux-x86_64/bin/phantomjs /usr/bin/phantomjs -/usr/bin/phantomjs -v \ No newline at end of file diff --git a/virtualization/Docker/setup_docker_prereqs b/virtualization/Docker/setup_docker_prereqs index 15504ea57aff2f..65acf92b855b4a 100755 --- a/virtualization/Docker/setup_docker_prereqs +++ b/virtualization/Docker/setup_docker_prereqs @@ -7,7 +7,6 @@ set -e INSTALL_TELLSTICK="${INSTALL_TELLSTICK:-yes}" INSTALL_OPENALPR="${INSTALL_OPENALPR:-yes}" INSTALL_LIBCEC="${INSTALL_LIBCEC:-yes}" -INSTALL_PHANTOMJS="${INSTALL_PHANTOMJS:-yes}" INSTALL_SSOCR="${INSTALL_SSOCR:-yes}" # Required debian packages for running hass or components @@ -59,10 +58,6 @@ if [ "$INSTALL_LIBCEC" == "yes" ]; then virtualization/Docker/scripts/libcec fi -if [ "$INSTALL_PHANTOMJS" == "yes" ]; then - virtualization/Docker/scripts/phantomjs -fi - if [ "$INSTALL_SSOCR" == "yes" ]; then virtualization/Docker/scripts/ssocr fi From 1b384c322a4fd60404bf344cdd352e571e213e20 Mon Sep 17 00:00:00 2001 From: Jason Hu Date: Mon, 13 Aug 2018 00:26:20 -0700 Subject: [PATCH 100/117] Remove remote.API from core.Config (#15951) * Use core.ApiConfig replace remote.API in core.Config * Move ApiConfig to http --- homeassistant/components/http/__init__.py | 28 ++++++++++++-- homeassistant/core.py | 4 +- tests/components/http/test_init.py | 45 +++++++++++++++++++++++ 3 files changed, 72 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index 9f1b5995839db2..c1d80667983192 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -8,6 +8,7 @@ import logging import os import ssl +from typing import Optional from aiohttp import web from aiohttp.web_exceptions import HTTPMovedPermanently @@ -16,7 +17,6 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, SERVER_PORT) import homeassistant.helpers.config_validation as cv -import homeassistant.remote as rem import homeassistant.util as hass_util from homeassistant.util.logging import HideSensitiveDataFilter from homeassistant.util import ssl as ssl_util @@ -82,6 +82,28 @@ }, extra=vol.ALLOW_EXTRA) +class ApiConfig: + """Configuration settings for API server.""" + + def __init__(self, host: str, port: Optional[int] = SERVER_PORT, + use_ssl: bool = False, + api_password: Optional[str] = None) -> None: + """Initialize a new API config object.""" + self.host = host + self.port = port + self.api_password = api_password + + if host.startswith(("http://", "https://")): + self.base_url = host + elif use_ssl: + self.base_url = "https://{}".format(host) + else: + self.base_url = "http://{}".format(host) + + if port is not None: + self.base_url += ':{}'.format(port) + + async def async_setup(hass, config): """Set up the HTTP API and debug interface.""" conf = config.get(DOMAIN) @@ -146,8 +168,8 @@ async def start_server(event): host = hass_util.get_local_ip() port = server_port - hass.config.api = rem.API(host, api_password, port, - ssl_certificate is not None) + hass.config.api = ApiConfig(host, port, ssl_certificate is not None, + api_password) return True diff --git a/homeassistant/core.py b/homeassistant/core.py index cc027c6f5d0d01..2b7a2479471d61 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -1145,8 +1145,8 @@ def __init__(self) -> None: # List of loaded components self.components = set() # type: set - # Remote.API object pointing at local API - self.api = None + # API (HTTP) server configuration + self.api = None # type: Optional[Any] # Directory that holds the configuration self.config_dir = None # type: Optional[str] diff --git a/tests/components/http/test_init.py b/tests/components/http/test_init.py index 2ffaf17bebcca1..c52f60a5f1b0fc 100644 --- a/tests/components/http/test_init.py +++ b/tests/components/http/test_init.py @@ -1,5 +1,6 @@ """The tests for the Home Assistant HTTP component.""" import logging +import unittest from homeassistant.setup import async_setup_component @@ -33,6 +34,50 @@ async def test_registering_view_while_running(hass, aiohttp_client, hass.http.register_view(TestView) +class TestApiConfig(unittest.TestCase): + """Test API configuration methods.""" + + def test_api_base_url_with_domain(hass): + """Test setting API URL with domain.""" + api_config = http.ApiConfig('example.com') + assert api_config.base_url == 'http://example.com:8123' + + def test_api_base_url_with_ip(hass): + """Test setting API URL with IP.""" + api_config = http.ApiConfig('1.1.1.1') + assert api_config.base_url == 'http://1.1.1.1:8123' + + def test_api_base_url_with_ip_and_port(hass): + """Test setting API URL with IP and port.""" + api_config = http.ApiConfig('1.1.1.1', 8124) + assert api_config.base_url == 'http://1.1.1.1:8124' + + def test_api_base_url_with_protocol(hass): + """Test setting API URL with protocol.""" + api_config = http.ApiConfig('https://example.com') + assert api_config.base_url == 'https://example.com:8123' + + def test_api_base_url_with_protocol_and_port(hass): + """Test setting API URL with protocol and port.""" + api_config = http.ApiConfig('https://example.com', 433) + assert api_config.base_url == 'https://example.com:433' + + def test_api_base_url_with_ssl_enable(hass): + """Test setting API URL with use_ssl enabled.""" + api_config = http.ApiConfig('example.com', use_ssl=True) + assert api_config.base_url == 'https://example.com:8123' + + def test_api_base_url_with_ssl_enable_and_port(hass): + """Test setting API URL with use_ssl enabled and port.""" + api_config = http.ApiConfig('1.1.1.1', use_ssl=True, port=8888) + assert api_config.base_url == 'https://1.1.1.1:8888' + + def test_api_base_url_with_protocol_and_ssl_enable(hass): + """Test setting API URL with specific protocol and use_ssl enabled.""" + api_config = http.ApiConfig('http://example.com', use_ssl=True) + assert api_config.base_url == 'http://example.com:8123' + + async def test_api_base_url_with_domain(hass): """Test setting API URL.""" result = await async_setup_component(hass, 'http', { From 899c2057b739977974ad72995d4d7e385a25c307 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 14 Aug 2018 08:20:17 +0200 Subject: [PATCH 101/117] Switch to intermediate Mozilla cert profile (#15957) * Allow choosing intermediate SSL profile * Fix tests --- homeassistant/components/http/__init__.py | 20 ++++++-- homeassistant/util/ssl.py | 56 ++++++++++++++++++++++- tests/components/http/test_init.py | 56 +++++++++++++++++++++++ tests/scripts/test_check_config.py | 4 +- 4 files changed, 130 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index c1d80667983192..9ba977f92f5c42 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -49,6 +49,10 @@ CONF_TRUSTED_NETWORKS = 'trusted_networks' CONF_LOGIN_ATTEMPTS_THRESHOLD = 'login_attempts_threshold' CONF_IP_BAN_ENABLED = 'ip_ban_enabled' +CONF_SSL_PROFILE = 'ssl_profile' + +SSL_MODERN = 'modern' +SSL_INTERMEDIATE = 'intermediate' _LOGGER = logging.getLogger(__name__) @@ -74,7 +78,9 @@ vol.Optional(CONF_LOGIN_ATTEMPTS_THRESHOLD, default=NO_LOGIN_ATTEMPT_THRESHOLD): vol.Any(cv.positive_int, NO_LOGIN_ATTEMPT_THRESHOLD), - vol.Optional(CONF_IP_BAN_ENABLED, default=True): cv.boolean + vol.Optional(CONF_IP_BAN_ENABLED, default=True): cv.boolean, + vol.Optional(CONF_SSL_PROFILE, default=SSL_MODERN): + vol.In([SSL_INTERMEDIATE, SSL_MODERN]), }) CONFIG_SCHEMA = vol.Schema({ @@ -123,6 +129,7 @@ async def async_setup(hass, config): trusted_networks = conf[CONF_TRUSTED_NETWORKS] is_ban_enabled = conf[CONF_IP_BAN_ENABLED] login_threshold = conf[CONF_LOGIN_ATTEMPTS_THRESHOLD] + ssl_profile = conf[CONF_SSL_PROFILE] if api_password is not None: logging.getLogger('aiohttp.access').addFilter( @@ -141,7 +148,8 @@ async def async_setup(hass, config): trusted_proxies=trusted_proxies, trusted_networks=trusted_networks, login_threshold=login_threshold, - is_ban_enabled=is_ban_enabled + is_ban_enabled=is_ban_enabled, + ssl_profile=ssl_profile, ) async def stop_server(event): @@ -181,7 +189,7 @@ def __init__(self, hass, api_password, ssl_certificate, ssl_peer_certificate, ssl_key, server_host, server_port, cors_origins, use_x_forwarded_for, trusted_proxies, trusted_networks, - login_threshold, is_ban_enabled): + login_threshold, is_ban_enabled, ssl_profile): """Initialize the HTTP Home Assistant server.""" app = self.app = web.Application( middlewares=[staticresource_middleware]) @@ -221,6 +229,7 @@ def __init__(self, hass, api_password, self.server_host = server_host self.server_port = server_port self.is_ban_enabled = is_ban_enabled + self.ssl_profile = ssl_profile self._handler = None self.server = None @@ -307,7 +316,10 @@ async def start(self): if self.ssl_certificate: try: - context = ssl_util.server_context() + if self.ssl_profile == SSL_INTERMEDIATE: + context = ssl_util.server_context_intermediate() + else: + context = ssl_util.server_context_modern() context.load_cert_chain(self.ssl_certificate, self.ssl_key) except OSError as error: _LOGGER.error("Could not read SSL certificate from %s: %s", diff --git a/homeassistant/util/ssl.py b/homeassistant/util/ssl.py index 392c5986c8914b..b78395cdb0d341 100644 --- a/homeassistant/util/ssl.py +++ b/homeassistant/util/ssl.py @@ -13,7 +13,7 @@ def client_context() -> ssl.SSLContext: return context -def server_context() -> ssl.SSLContext: +def server_context_modern() -> ssl.SSLContext: """Return an SSL context following the Mozilla recommendations. TLS configuration follows the best-practice guidelines specified here: @@ -37,4 +37,58 @@ def server_context() -> ssl.SSLContext: "ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:" "ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256" ) + + return context + + +def server_context_intermediate() -> ssl.SSLContext: + """Return an SSL context following the Mozilla recommendations. + + TLS configuration follows the best-practice guidelines specified here: + https://wiki.mozilla.org/Security/Server_Side_TLS + Intermediate guidelines are followed. + """ + context = ssl.SSLContext(ssl.PROTOCOL_TLS) # pylint: disable=no-member + + context.options |= ( + ssl.OP_NO_SSLv2 | ssl.OP_NO_SSLv3 | + ssl.OP_CIPHER_SERVER_PREFERENCE + ) + if hasattr(ssl, 'OP_NO_COMPRESSION'): + context.options |= ssl.OP_NO_COMPRESSION + + context.set_ciphers( + "ECDHE-ECDSA-CHACHA20-POLY1305:" + "ECDHE-RSA-CHACHA20-POLY1305:" + "ECDHE-ECDSA-AES128-GCM-SHA256:" + "ECDHE-RSA-AES128-GCM-SHA256:" + "ECDHE-ECDSA-AES256-GCM-SHA384:" + "ECDHE-RSA-AES256-GCM-SHA384:" + "DHE-RSA-AES128-GCM-SHA256:" + "DHE-RSA-AES256-GCM-SHA384:" + "ECDHE-ECDSA-AES128-SHA256:" + "ECDHE-RSA-AES128-SHA256:" + "ECDHE-ECDSA-AES128-SHA:" + "ECDHE-RSA-AES256-SHA384:" + "ECDHE-RSA-AES128-SHA:" + "ECDHE-ECDSA-AES256-SHA384:" + "ECDHE-ECDSA-AES256-SHA:" + "ECDHE-RSA-AES256-SHA:" + "DHE-RSA-AES128-SHA256:" + "DHE-RSA-AES128-SHA:" + "DHE-RSA-AES256-SHA256:" + "DHE-RSA-AES256-SHA:" + "ECDHE-ECDSA-DES-CBC3-SHA:" + "ECDHE-RSA-DES-CBC3-SHA:" + "EDH-RSA-DES-CBC3-SHA:" + "AES128-GCM-SHA256:" + "AES256-GCM-SHA384:" + "AES128-SHA256:" + "AES256-SHA256:" + "AES128-SHA:" + "AES256-SHA:" + "DES-CBC3-SHA:" + "!DSS" + ) + return context diff --git a/tests/components/http/test_init.py b/tests/components/http/test_init.py index c52f60a5f1b0fc..9f6441c52386f3 100644 --- a/tests/components/http/test_init.py +++ b/tests/components/http/test_init.py @@ -1,10 +1,13 @@ """The tests for the Home Assistant HTTP component.""" import logging import unittest +from unittest.mock import patch from homeassistant.setup import async_setup_component import homeassistant.components.http as http +from homeassistant.util.ssl import ( + server_context_modern, server_context_intermediate) class TestView(http.HomeAssistantView): @@ -169,3 +172,56 @@ async def test_proxy_config_only_trust_proxies(hass): http.CONF_TRUSTED_PROXIES: ['127.0.0.1'] } }) is not True + + +async def test_ssl_profile_defaults_modern(hass): + """Test default ssl profile.""" + assert await async_setup_component(hass, 'http', {}) is True + + hass.http.ssl_certificate = 'bla' + + with patch('ssl.SSLContext.load_cert_chain'), \ + patch('homeassistant.util.ssl.server_context_modern', + side_effect=server_context_modern) as mock_context: + await hass.async_start() + await hass.async_block_till_done() + + assert len(mock_context.mock_calls) == 1 + + +async def test_ssl_profile_change_intermediate(hass): + """Test setting ssl profile to intermediate.""" + assert await async_setup_component(hass, 'http', { + 'http': { + 'ssl_profile': 'intermediate' + } + }) is True + + hass.http.ssl_certificate = 'bla' + + with patch('ssl.SSLContext.load_cert_chain'), \ + patch('homeassistant.util.ssl.server_context_intermediate', + side_effect=server_context_intermediate) as mock_context: + await hass.async_start() + await hass.async_block_till_done() + + assert len(mock_context.mock_calls) == 1 + + +async def test_ssl_profile_change_modern(hass): + """Test setting ssl profile to modern.""" + assert await async_setup_component(hass, 'http', { + 'http': { + 'ssl_profile': 'modern' + } + }) is True + + hass.http.ssl_certificate = 'bla' + + with patch('ssl.SSLContext.load_cert_chain'), \ + patch('homeassistant.util.ssl.server_context_modern', + side_effect=server_context_modern) as mock_context: + await hass.async_start() + await hass.async_block_till_done() + + assert len(mock_context.mock_calls) == 1 diff --git a/tests/scripts/test_check_config.py b/tests/scripts/test_check_config.py index 59d8e27a672ee7..532197b407227b 100644 --- a/tests/scripts/test_check_config.py +++ b/tests/scripts/test_check_config.py @@ -159,7 +159,9 @@ def test_secrets(self, isfile_patch): 'login_attempts_threshold': -1, 'server_host': '0.0.0.0', 'server_port': 8123, - 'trusted_networks': []} + 'trusted_networks': [], + 'ssl_profile': 'modern', + } assert res['secret_cache'] == {secrets_path: {'http_pw': 'abc123'}} assert res['secrets'] == {'http_pw': 'abc123'} assert normalize_yaml_files(res) == [ From f5df567d099f6bdef1e0b53b38d36e48e9f58c0e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 14 Aug 2018 21:14:12 +0200 Subject: [PATCH 102/117] Use JWT for access tokens (#15972) * Use JWT for access tokens * Update requirements * Improvements --- homeassistant/auth/__init__.py | 64 +++++++++++++------ homeassistant/auth/auth_store.py | 56 ++++++++-------- homeassistant/auth/models.py | 22 +------ homeassistant/components/auth/__init__.py | 6 +- homeassistant/components/http/auth.py | 6 +- homeassistant/components/websocket_api.py | 9 +-- homeassistant/package_constraints.txt | 1 + requirements_all.txt | 1 + setup.py | 1 + tests/auth/test_init.py | 45 ++++--------- tests/common.py | 14 ++-- tests/components/auth/test_init.py | 24 +++++-- tests/components/auth/test_init_link_user.py | 2 +- tests/components/config/test_auth.py | 16 +++-- .../test_auth_provider_homeassistant.py | 38 ++++++++--- tests/components/conftest.py | 2 +- tests/components/hassio/test_init.py | 6 +- tests/components/http/test_auth.py | 8 ++- tests/components/test_api.py | 22 +++++-- tests/components/test_websocket_api.py | 15 +++-- 20 files changed, 203 insertions(+), 155 deletions(-) diff --git a/homeassistant/auth/__init__.py b/homeassistant/auth/__init__.py index 9695e77f6f1098..148f97702e3a2c 100644 --- a/homeassistant/auth/__init__.py +++ b/homeassistant/auth/__init__.py @@ -4,10 +4,12 @@ from collections import OrderedDict from typing import List, Awaitable +import jwt + from homeassistant import data_entry_flow from homeassistant.core import callback, HomeAssistant +from homeassistant.util import dt as dt_util -from . import models from . import auth_store from .providers import auth_provider_from_config @@ -54,7 +56,6 @@ def __init__(self, hass, store, providers): self.login_flow = data_entry_flow.FlowManager( hass, self._async_create_login_flow, self._async_finish_login_flow) - self._access_tokens = OrderedDict() @property def active(self): @@ -181,35 +182,56 @@ async def async_create_refresh_token(self, user, client_id=None): return await self._store.async_create_refresh_token(user, client_id) - async def async_get_refresh_token(self, token): + async def async_get_refresh_token(self, token_id): + """Get refresh token by id.""" + return await self._store.async_get_refresh_token(token_id) + + async def async_get_refresh_token_by_token(self, token): """Get refresh token by token.""" - return await self._store.async_get_refresh_token(token) + return await self._store.async_get_refresh_token_by_token(token) @callback def async_create_access_token(self, refresh_token): """Create a new access token.""" - access_token = models.AccessToken(refresh_token=refresh_token) - self._access_tokens[access_token.token] = access_token - return access_token - - @callback - def async_get_access_token(self, token): - """Get an access token.""" - tkn = self._access_tokens.get(token) + # pylint: disable=no-self-use + return jwt.encode({ + 'iss': refresh_token.id, + 'iat': dt_util.utcnow(), + 'exp': dt_util.utcnow() + refresh_token.access_token_expiration, + }, refresh_token.jwt_key, algorithm='HS256').decode() + + async def async_validate_access_token(self, token): + """Return if an access token is valid.""" + try: + unverif_claims = jwt.decode(token, verify=False) + except jwt.InvalidTokenError: + return None - if tkn is None: - _LOGGER.debug('Attempt to get non-existing access token') + refresh_token = await self.async_get_refresh_token( + unverif_claims.get('iss')) + + if refresh_token is None: + jwt_key = '' + issuer = '' + else: + jwt_key = refresh_token.jwt_key + issuer = refresh_token.id + + try: + jwt.decode( + token, + jwt_key, + leeway=10, + issuer=issuer, + algorithms=['HS256'] + ) + except jwt.InvalidTokenError: return None - if tkn.expired or not tkn.refresh_token.user.is_active: - if tkn.expired: - _LOGGER.debug('Attempt to get expired access token') - else: - _LOGGER.debug('Attempt to get access token for inactive user') - self._access_tokens.pop(token) + if not refresh_token.user.is_active: return None - return tkn + return refresh_token async def _async_create_login_flow(self, handler, *, context, data): """Create a login flow.""" diff --git a/homeassistant/auth/auth_store.py b/homeassistant/auth/auth_store.py index 8fd66d4bbb7dde..806cd109d78e64 100644 --- a/homeassistant/auth/auth_store.py +++ b/homeassistant/auth/auth_store.py @@ -1,6 +1,7 @@ """Storage for auth models.""" from collections import OrderedDict from datetime import timedelta +import hmac from homeassistant.util import dt as dt_util @@ -110,22 +111,36 @@ async def async_remove_credentials(self, credentials): async def async_create_refresh_token(self, user, client_id=None): """Create a new token for a user.""" refresh_token = models.RefreshToken(user=user, client_id=client_id) - user.refresh_tokens[refresh_token.token] = refresh_token + user.refresh_tokens[refresh_token.id] = refresh_token await self.async_save() return refresh_token - async def async_get_refresh_token(self, token): - """Get refresh token by token.""" + async def async_get_refresh_token(self, token_id): + """Get refresh token by id.""" if self._users is None: await self.async_load() for user in self._users.values(): - refresh_token = user.refresh_tokens.get(token) + refresh_token = user.refresh_tokens.get(token_id) if refresh_token is not None: return refresh_token return None + async def async_get_refresh_token_by_token(self, token): + """Get refresh token by token.""" + if self._users is None: + await self.async_load() + + found = None + + for user in self._users.values(): + for refresh_token in user.refresh_tokens.values(): + if hmac.compare_digest(refresh_token.token, token): + found = refresh_token + + return found + async def async_load(self): """Load the users.""" data = await self._store.async_load() @@ -153,9 +168,11 @@ async def async_load(self): data=cred_dict['data'], )) - refresh_tokens = OrderedDict() - for rt_dict in data['refresh_tokens']: + # Filter out the old keys that don't have jwt_key (pre-0.76) + if 'jwt_key' not in rt_dict: + continue + token = models.RefreshToken( id=rt_dict['id'], user=users[rt_dict['user_id']], @@ -164,18 +181,9 @@ async def async_load(self): access_token_expiration=timedelta( seconds=rt_dict['access_token_expiration']), token=rt_dict['token'], + jwt_key=rt_dict['jwt_key'] ) - refresh_tokens[token.id] = token - users[rt_dict['user_id']].refresh_tokens[token.token] = token - - for ac_dict in data['access_tokens']: - refresh_token = refresh_tokens[ac_dict['refresh_token_id']] - token = models.AccessToken( - refresh_token=refresh_token, - created_at=dt_util.parse_datetime(ac_dict['created_at']), - token=ac_dict['token'], - ) - refresh_token.access_tokens.append(token) + users[rt_dict['user_id']].refresh_tokens[token.id] = token self._users = users @@ -213,27 +221,15 @@ async def async_save(self): 'access_token_expiration': refresh_token.access_token_expiration.total_seconds(), 'token': refresh_token.token, + 'jwt_key': refresh_token.jwt_key, } for user in self._users.values() for refresh_token in user.refresh_tokens.values() ] - access_tokens = [ - { - 'id': user.id, - 'refresh_token_id': refresh_token.id, - 'created_at': access_token.created_at.isoformat(), - 'token': access_token.token, - } - for user in self._users.values() - for refresh_token in user.refresh_tokens.values() - for access_token in refresh_token.access_tokens - ] - data = { 'users': users, 'credentials': credentials, - 'access_tokens': access_tokens, 'refresh_tokens': refresh_tokens, } diff --git a/homeassistant/auth/models.py b/homeassistant/auth/models.py index 38e054dc7cf71a..3f49c56bce67eb 100644 --- a/homeassistant/auth/models.py +++ b/homeassistant/auth/models.py @@ -39,26 +39,8 @@ class RefreshToken: default=ACCESS_TOKEN_EXPIRATION) token = attr.ib(type=str, default=attr.Factory(lambda: generate_secret(64))) - access_tokens = attr.ib(type=list, default=attr.Factory(list), cmp=False) - - -@attr.s(slots=True) -class AccessToken: - """Access token to access the API. - - These will only ever be stored in memory and not be persisted. - """ - - refresh_token = attr.ib(type=RefreshToken) - created_at = attr.ib(type=datetime, default=attr.Factory(dt_util.utcnow)) - token = attr.ib(type=str, - default=attr.Factory(generate_secret)) - - @property - def expired(self): - """Return if this token has expired.""" - expires = self.created_at + self.refresh_token.access_token_expiration - return dt_util.utcnow() > expires + jwt_key = attr.ib(type=str, + default=attr.Factory(lambda: generate_secret(64))) @attr.s(slots=True) diff --git a/homeassistant/components/auth/__init__.py b/homeassistant/components/auth/__init__.py index 0b2b4fb1a2ec5b..102bfe58b55a69 100644 --- a/homeassistant/components/auth/__init__.py +++ b/homeassistant/components/auth/__init__.py @@ -155,7 +155,7 @@ async def _async_handle_auth_code(self, hass, data): access_token = hass.auth.async_create_access_token(refresh_token) return self.json({ - 'access_token': access_token.token, + 'access_token': access_token, 'token_type': 'Bearer', 'refresh_token': refresh_token.token, 'expires_in': @@ -178,7 +178,7 @@ async def _async_handle_refresh_token(self, hass, data): 'error': 'invalid_request', }, status_code=400) - refresh_token = await hass.auth.async_get_refresh_token(token) + refresh_token = await hass.auth.async_get_refresh_token_by_token(token) if refresh_token is None: return self.json({ @@ -193,7 +193,7 @@ async def _async_handle_refresh_token(self, hass, data): access_token = hass.auth.async_create_access_token(refresh_token) return self.json({ - 'access_token': access_token.token, + 'access_token': access_token, 'token_type': 'Bearer', 'expires_in': int(refresh_token.access_token_expiration.total_seconds()), diff --git a/homeassistant/components/http/auth.py b/homeassistant/components/http/auth.py index 77621e3bc7c217..d01d1b50c5acc9 100644 --- a/homeassistant/components/http/auth.py +++ b/homeassistant/components/http/auth.py @@ -106,11 +106,11 @@ async def async_validate_auth_header(request, api_password=None): if auth_type == 'Bearer': hass = request.app['hass'] - access_token = hass.auth.async_get_access_token(auth_val) - if access_token is None: + refresh_token = await hass.auth.async_validate_access_token(auth_val) + if refresh_token is None: return False - request['hass_user'] = access_token.refresh_token.user + request['hass_user'] = refresh_token.user return True if auth_type == 'Basic' and api_password is not None: diff --git a/homeassistant/components/websocket_api.py b/homeassistant/components/websocket_api.py index d9c92fa357fe42..532f3672df478a 100644 --- a/homeassistant/components/websocket_api.py +++ b/homeassistant/components/websocket_api.py @@ -352,11 +352,12 @@ def handle_hass_stop(event): if self.hass.auth.active and 'access_token' in msg: self.debug("Received access_token") - token = self.hass.auth.async_get_access_token( - msg['access_token']) - authenticated = token is not None + refresh_token = \ + await self.hass.auth.async_validate_access_token( + msg['access_token']) + authenticated = refresh_token is not None if authenticated: - request['hass_user'] = token.refresh_token.user + request['hass_user'] = refresh_token.user elif ((not self.hass.auth.active or self.hass.auth.support_legacy) and diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 29e10838f2196f..3aa1e3643c6fa7 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -4,6 +4,7 @@ async_timeout==3.0.0 attrs==18.1.0 certifi>=2018.04.16 jinja2>=2.10 +PyJWT==1.6.4 pip>=8.0.3 pytz>=2018.04 pyyaml>=3.13,<4 diff --git a/requirements_all.txt b/requirements_all.txt index 0e6d7e1ac0727c..3f50e50d19afc7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -5,6 +5,7 @@ async_timeout==3.0.0 attrs==18.1.0 certifi>=2018.04.16 jinja2>=2.10 +PyJWT==1.6.4 pip>=8.0.3 pytz>=2018.04 pyyaml>=3.13,<4 diff --git a/setup.py b/setup.py index b319df9067d41b..bd1e70aa8ae6f1 100755 --- a/setup.py +++ b/setup.py @@ -38,6 +38,7 @@ 'attrs==18.1.0', 'certifi>=2018.04.16', 'jinja2>=2.10', + 'PyJWT==1.6.4', 'pip>=8.0.3', 'pytz>=2018.04', 'pyyaml>=3.13,<4', diff --git a/tests/auth/test_init.py b/tests/auth/test_init.py index cad4bbdbd7164f..da5daca7cf63c5 100644 --- a/tests/auth/test_init.py +++ b/tests/auth/test_init.py @@ -199,9 +199,7 @@ async def test_saving_loading(hass, hass_storage): }) user = await manager.async_get_or_create_user(step['result']) await manager.async_activate_user(user) - refresh_token = await manager.async_create_refresh_token(user, CLIENT_ID) - - manager.async_create_access_token(refresh_token) + await manager.async_create_refresh_token(user, CLIENT_ID) await flush_store(manager._store._store) @@ -211,30 +209,6 @@ async def test_saving_loading(hass, hass_storage): assert users[0] == user -def test_access_token_expired(): - """Test that the expired property on access tokens work.""" - refresh_token = auth_models.RefreshToken( - user=None, - client_id='bla' - ) - - access_token = auth_models.AccessToken( - refresh_token=refresh_token - ) - - assert access_token.expired is False - - with patch('homeassistant.util.dt.utcnow', - return_value=dt_util.utcnow() + - auth_const.ACCESS_TOKEN_EXPIRATION): - assert access_token.expired is True - - almost_exp = \ - dt_util.utcnow() + auth_const.ACCESS_TOKEN_EXPIRATION - timedelta(1) - with patch('homeassistant.util.dt.utcnow', return_value=almost_exp): - assert access_token.expired is False - - async def test_cannot_retrieve_expired_access_token(hass): """Test that we cannot retrieve expired access tokens.""" manager = await auth.auth_manager_from_config(hass, []) @@ -244,15 +218,20 @@ async def test_cannot_retrieve_expired_access_token(hass): assert refresh_token.client_id == CLIENT_ID access_token = manager.async_create_access_token(refresh_token) - assert manager.async_get_access_token(access_token.token) is access_token + assert ( + await manager.async_validate_access_token(access_token) + is refresh_token + ) with patch('homeassistant.util.dt.utcnow', - return_value=dt_util.utcnow() + - auth_const.ACCESS_TOKEN_EXPIRATION): - assert manager.async_get_access_token(access_token.token) is None + return_value=dt_util.utcnow() - + auth_const.ACCESS_TOKEN_EXPIRATION - timedelta(seconds=11)): + access_token = manager.async_create_access_token(refresh_token) - # Even with unpatched time, it should have been removed from manager - assert manager.async_get_access_token(access_token.token) is None + assert ( + await manager.async_validate_access_token(access_token) + is None + ) async def test_generating_system_user(hass): diff --git a/tests/common.py b/tests/common.py index df333cca735813..81e4774ccd4779 100644 --- a/tests/common.py +++ b/tests/common.py @@ -314,12 +314,18 @@ def mock_registry(hass, mock_entries=None): class MockUser(auth_models.User): """Mock a user in Home Assistant.""" - def __init__(self, id='mock-id', is_owner=False, is_active=True, + def __init__(self, id=None, is_owner=False, is_active=True, name='Mock User', system_generated=False): """Initialize mock user.""" - super().__init__( - id=id, is_owner=is_owner, is_active=is_active, name=name, - system_generated=system_generated) + kwargs = { + 'is_owner': is_owner, + 'is_active': is_active, + 'name': name, + 'system_generated': system_generated + } + if id is not None: + kwargs['id'] = id + super().__init__(**kwargs) def add_to_hass(self, hass): """Test helper to add entry to hass.""" diff --git a/tests/components/auth/test_init.py b/tests/components/auth/test_init.py index eea768c96a0974..f1a1bb5bd3cc97 100644 --- a/tests/components/auth/test_init.py +++ b/tests/components/auth/test_init.py @@ -44,7 +44,10 @@ async def test_login_new_user_and_trying_refresh_token(hass, aiohttp_client): assert resp.status == 200 tokens = await resp.json() - assert hass.auth.async_get_access_token(tokens['access_token']) is not None + assert ( + await hass.auth.async_validate_access_token(tokens['access_token']) + is not None + ) # Use refresh token to get more tokens. resp = await client.post('/auth/token', data={ @@ -56,7 +59,10 @@ async def test_login_new_user_and_trying_refresh_token(hass, aiohttp_client): assert resp.status == 200 tokens = await resp.json() assert 'refresh_token' not in tokens - assert hass.auth.async_get_access_token(tokens['access_token']) is not None + assert ( + await hass.auth.async_validate_access_token(tokens['access_token']) + is not None + ) # Test using access token to hit API. resp = await client.get('/api/') @@ -98,7 +104,9 @@ async def test_ws_current_user(hass, hass_ws_client, hass_access_token): } }) - user = hass_access_token.refresh_token.user + refresh_token = await hass.auth.async_validate_access_token( + hass_access_token) + user = refresh_token.user credential = Credentials(auth_provider_type='homeassistant', auth_provider_id=None, data={}, id='test-id') @@ -169,7 +177,10 @@ async def test_refresh_token_system_generated(hass, aiohttp_client): assert resp.status == 200 tokens = await resp.json() - assert hass.auth.async_get_access_token(tokens['access_token']) is not None + assert ( + await hass.auth.async_validate_access_token(tokens['access_token']) + is not None + ) async def test_refresh_token_different_client_id(hass, aiohttp_client): @@ -208,4 +219,7 @@ async def test_refresh_token_different_client_id(hass, aiohttp_client): assert resp.status == 200 tokens = await resp.json() - assert hass.auth.async_get_access_token(tokens['access_token']) is not None + assert ( + await hass.auth.async_validate_access_token(tokens['access_token']) + is not None + ) diff --git a/tests/components/auth/test_init_link_user.py b/tests/components/auth/test_init_link_user.py index 13515db87fab9c..e209e0ee85696b 100644 --- a/tests/components/auth/test_init_link_user.py +++ b/tests/components/auth/test_init_link_user.py @@ -52,7 +52,7 @@ async def async_get_code(hass, aiohttp_client): 'user': user, 'code': step['result'], 'client': client, - 'access_token': access_token.token, + 'access_token': access_token, } diff --git a/tests/components/config/test_auth.py b/tests/components/config/test_auth.py index fe8f351955f8a5..cd04eedf08eb29 100644 --- a/tests/components/config/test_auth.py +++ b/tests/components/config/test_auth.py @@ -122,11 +122,13 @@ async def test_delete_unable_self_account(hass, hass_ws_client, hass_access_token): """Test we cannot delete our own account.""" client = await hass_ws_client(hass, hass_access_token) + refresh_token = await hass.auth.async_validate_access_token( + hass_access_token) await client.send_json({ 'id': 5, 'type': auth_config.WS_TYPE_DELETE, - 'user_id': hass_access_token.refresh_token.user.id, + 'user_id': refresh_token.user.id, }) result = await client.receive_json() @@ -137,7 +139,9 @@ async def test_delete_unable_self_account(hass, hass_ws_client, async def test_delete_unknown_user(hass, hass_ws_client, hass_access_token): """Test we cannot delete an unknown user.""" client = await hass_ws_client(hass, hass_access_token) - hass_access_token.refresh_token.user.is_owner = True + refresh_token = await hass.auth.async_validate_access_token( + hass_access_token) + refresh_token.user.is_owner = True await client.send_json({ 'id': 5, @@ -153,7 +157,9 @@ async def test_delete_unknown_user(hass, hass_ws_client, hass_access_token): async def test_delete(hass, hass_ws_client, hass_access_token): """Test delete command works.""" client = await hass_ws_client(hass, hass_access_token) - hass_access_token.refresh_token.user.is_owner = True + refresh_token = await hass.auth.async_validate_access_token( + hass_access_token) + refresh_token.user.is_owner = True test_user = MockUser( id='efg', ).add_to_hass(hass) @@ -174,7 +180,9 @@ async def test_delete(hass, hass_ws_client, hass_access_token): async def test_create(hass, hass_ws_client, hass_access_token): """Test create command works.""" client = await hass_ws_client(hass, hass_access_token) - hass_access_token.refresh_token.user.is_owner = True + refresh_token = await hass.auth.async_validate_access_token( + hass_access_token) + refresh_token.user.is_owner = True assert len(await hass.auth.async_get_users()) == 1 diff --git a/tests/components/config/test_auth_provider_homeassistant.py b/tests/components/config/test_auth_provider_homeassistant.py index cd2cbc44539967..a374083c2aba9e 100644 --- a/tests/components/config/test_auth_provider_homeassistant.py +++ b/tests/components/config/test_auth_provider_homeassistant.py @@ -9,7 +9,7 @@ @pytest.fixture(autouse=True) -def setup_config(hass, aiohttp_client): +def setup_config(hass): """Fixture that sets up the auth provider homeassistant module.""" hass.loop.run_until_complete(register_auth_provider(hass, { 'type': 'homeassistant' @@ -22,7 +22,9 @@ async def test_create_auth_system_generated_user(hass, hass_access_token, """Test we can't add auth to system generated users.""" system_user = MockUser(system_generated=True).add_to_hass(hass) client = await hass_ws_client(hass, hass_access_token) - hass_access_token.refresh_token.user.is_owner = True + refresh_token = await hass.auth.async_validate_access_token( + hass_access_token) + refresh_token.user.is_owner = True await client.send_json({ 'id': 5, @@ -47,7 +49,9 @@ async def test_create_auth_unknown_user(hass_ws_client, hass, hass_access_token): """Test create pointing at unknown user.""" client = await hass_ws_client(hass, hass_access_token) - hass_access_token.refresh_token.user.is_owner = True + refresh_token = await hass.auth.async_validate_access_token( + hass_access_token) + refresh_token.user.is_owner = True await client.send_json({ 'id': 5, @@ -86,7 +90,9 @@ async def test_create_auth(hass, hass_ws_client, hass_access_token, """Test create auth command works.""" client = await hass_ws_client(hass, hass_access_token) user = MockUser().add_to_hass(hass) - hass_access_token.refresh_token.user.is_owner = True + refresh_token = await hass.auth.async_validate_access_token( + hass_access_token) + refresh_token.user.is_owner = True assert len(user.credentials) == 0 @@ -117,7 +123,9 @@ async def test_create_auth_duplicate_username(hass, hass_ws_client, """Test we can't create auth with a duplicate username.""" client = await hass_ws_client(hass, hass_access_token) user = MockUser().add_to_hass(hass) - hass_access_token.refresh_token.user.is_owner = True + refresh_token = await hass.auth.async_validate_access_token( + hass_access_token) + refresh_token.user.is_owner = True hass_storage[prov_ha.STORAGE_KEY] = { 'version': 1, @@ -145,7 +153,9 @@ async def test_delete_removes_just_auth(hass_ws_client, hass, hass_storage, hass_access_token): """Test deleting an auth without being connected to a user.""" client = await hass_ws_client(hass, hass_access_token) - hass_access_token.refresh_token.user.is_owner = True + refresh_token = await hass.auth.async_validate_access_token( + hass_access_token) + refresh_token.user.is_owner = True hass_storage[prov_ha.STORAGE_KEY] = { 'version': 1, @@ -171,7 +181,9 @@ async def test_delete_removes_credential(hass, hass_ws_client, hass_access_token, hass_storage): """Test deleting auth that is connected to a user.""" client = await hass_ws_client(hass, hass_access_token) - hass_access_token.refresh_token.user.is_owner = True + refresh_token = await hass.auth.async_validate_access_token( + hass_access_token) + refresh_token.user.is_owner = True user = MockUser().add_to_hass(hass) user.credentials.append( @@ -216,7 +228,9 @@ async def test_delete_requires_owner(hass, hass_ws_client, hass_access_token): async def test_delete_unknown_auth(hass, hass_ws_client, hass_access_token): """Test trying to delete an unknown auth username.""" client = await hass_ws_client(hass, hass_access_token) - hass_access_token.refresh_token.user.is_owner = True + refresh_token = await hass.auth.async_validate_access_token( + hass_access_token) + refresh_token.user.is_owner = True await client.send_json({ 'id': 5, @@ -240,7 +254,9 @@ async def test_change_password(hass, hass_ws_client, hass_access_token): 'username': 'test-user' }) - user = hass_access_token.refresh_token.user + refresh_token = await hass.auth.async_validate_access_token( + hass_access_token) + user = refresh_token.user await hass.auth.async_link_user(user, credentials) client = await hass_ws_client(hass, hass_access_token) @@ -268,7 +284,9 @@ async def test_change_password_wrong_pw(hass, hass_ws_client, 'username': 'test-user' }) - user = hass_access_token.refresh_token.user + refresh_token = await hass.auth.async_validate_access_token( + hass_access_token) + user = refresh_token.user await hass.auth.async_link_user(user, credentials) client = await hass_ws_client(hass, hass_access_token) diff --git a/tests/components/conftest.py b/tests/components/conftest.py index 5f6a17a4101cb1..bb9b643296e678 100644 --- a/tests/components/conftest.py +++ b/tests/components/conftest.py @@ -28,7 +28,7 @@ async def create_client(hass, access_token=None): await websocket.send_json({ 'type': websocket_api.TYPE_AUTH, - 'access_token': access_token.token + 'access_token': access_token }) auth_ok = await websocket.receive_json() diff --git a/tests/components/hassio/test_init.py b/tests/components/hassio/test_init.py index b19756697311e0..4fd59dd3f7aaeb 100644 --- a/tests/components/hassio/test_init.py +++ b/tests/components/hassio/test_init.py @@ -106,7 +106,11 @@ async def test_setup_api_push_api_data_default(hass, aioclient_mock, ) assert hassio_user is not None assert hassio_user.system_generated - assert refresh_token in hassio_user.refresh_tokens + for token in hassio_user.refresh_tokens.values(): + if token.token == refresh_token: + break + else: + assert False, 'refresh token not found' async def test_setup_api_push_api_data_no_auth(hass, aioclient_mock, diff --git a/tests/components/http/test_auth.py b/tests/components/http/test_auth.py index 31cba79a6c8758..8e7a62e2e9faa5 100644 --- a/tests/components/http/test_auth.py +++ b/tests/components/http/test_auth.py @@ -156,9 +156,9 @@ async def test_access_with_trusted_ip(app2, aiohttp_client): async def test_auth_active_access_with_access_token_in_header( - app, aiohttp_client, hass_access_token): + hass, app, aiohttp_client, hass_access_token): """Test access with access token in header.""" - token = hass_access_token.token + token = hass_access_token setup_auth(app, [], True, api_password=None) client = await aiohttp_client(app) @@ -182,7 +182,9 @@ async def test_auth_active_access_with_access_token_in_header( '/', headers={'Authorization': 'BEARER {}'.format(token)}) assert req.status == 401 - hass_access_token.refresh_token.user.is_active = False + refresh_token = await hass.auth.async_validate_access_token( + hass_access_token) + refresh_token.user.is_active = False req = await client.get( '/', headers={'Authorization': 'Bearer {}'.format(token)}) assert req.status == 401 diff --git a/tests/components/test_api.py b/tests/components/test_api.py index 09dc27e97c1fa1..2be1168b86a829 100644 --- a/tests/components/test_api.py +++ b/tests/components/test_api.py @@ -448,13 +448,15 @@ def listener(event): await mock_api_client.post( const.URL_API_EVENTS_EVENT.format("test.event"), headers={ - 'authorization': 'Bearer {}'.format(hass_access_token.token) + 'authorization': 'Bearer {}'.format(hass_access_token) }) await hass.async_block_till_done() + refresh_token = await hass.auth.async_validate_access_token( + hass_access_token) + assert len(test_value) == 1 - assert test_value[0].context.user_id == \ - hass_access_token.refresh_token.user.id + assert test_value[0].context.user_id == refresh_token.user.id async def test_api_call_service_context(hass, mock_api_client, @@ -465,12 +467,15 @@ async def test_api_call_service_context(hass, mock_api_client, await mock_api_client.post( '/api/services/test_domain/test_service', headers={ - 'authorization': 'Bearer {}'.format(hass_access_token.token) + 'authorization': 'Bearer {}'.format(hass_access_token) }) await hass.async_block_till_done() + refresh_token = await hass.auth.async_validate_access_token( + hass_access_token) + assert len(calls) == 1 - assert calls[0].context.user_id == hass_access_token.refresh_token.user.id + assert calls[0].context.user_id == refresh_token.user.id async def test_api_set_state_context(hass, mock_api_client, hass_access_token): @@ -481,8 +486,11 @@ async def test_api_set_state_context(hass, mock_api_client, hass_access_token): 'state': 'on' }, headers={ - 'authorization': 'Bearer {}'.format(hass_access_token.token) + 'authorization': 'Bearer {}'.format(hass_access_token) }) + refresh_token = await hass.auth.async_validate_access_token( + hass_access_token) + state = hass.states.get('light.kitchen') - assert state.context.user_id == hass_access_token.refresh_token.user.id + assert state.context.user_id == refresh_token.user.id diff --git a/tests/components/test_websocket_api.py b/tests/components/test_websocket_api.py index 1fac1af9f64eb2..199a9d804f83f2 100644 --- a/tests/components/test_websocket_api.py +++ b/tests/components/test_websocket_api.py @@ -334,7 +334,7 @@ async def test_auth_active_with_token(hass, aiohttp_client, hass_access_token): await ws.send_json({ 'type': wapi.TYPE_AUTH, - 'access_token': hass_access_token.token + 'access_token': hass_access_token }) auth_msg = await ws.receive_json() @@ -344,7 +344,9 @@ async def test_auth_active_with_token(hass, aiohttp_client, hass_access_token): async def test_auth_active_user_inactive(hass, aiohttp_client, hass_access_token): """Test authenticating with a token.""" - hass_access_token.refresh_token.user.is_active = False + 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 @@ -361,7 +363,7 @@ async def test_auth_active_user_inactive(hass, aiohttp_client, await ws.send_json({ 'type': wapi.TYPE_AUTH, - 'access_token': hass_access_token.token + 'access_token': hass_access_token }) auth_msg = await ws.receive_json() @@ -465,7 +467,7 @@ async def test_call_service_context_with_user(hass, aiohttp_client, await ws.send_json({ 'type': wapi.TYPE_AUTH, - 'access_token': hass_access_token.token + 'access_token': hass_access_token }) auth_msg = await ws.receive_json() @@ -484,12 +486,15 @@ async def test_call_service_context_with_user(hass, aiohttp_client, 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 == hass_access_token.refresh_token.user.id + assert call.context.user_id == refresh_token.user.id async def test_call_service_context_no_user(hass, aiohttp_client): From 1777270aa2044f0468b877ceee2c3861e463a921 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 14 Aug 2018 22:02:01 +0200 Subject: [PATCH 103/117] Pin crypto (#15978) * Pin crypto * Fix PyJWT import once --- homeassistant/components/notify/html5.py | 2 +- homeassistant/package_constraints.txt | 1 + requirements_all.txt | 4 +--- requirements_test_all.txt | 3 --- setup.py | 2 ++ 5 files changed, 5 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/notify/html5.py b/homeassistant/components/notify/html5.py index e280aa67e40a4b..1ed5047200419d 100644 --- a/homeassistant/components/notify/html5.py +++ b/homeassistant/components/notify/html5.py @@ -26,7 +26,7 @@ from homeassistant.helpers import config_validation as cv from homeassistant.util import ensure_unique_string -REQUIREMENTS = ['pywebpush==1.6.0', 'PyJWT==1.6.0'] +REQUIREMENTS = ['pywebpush==1.6.0'] DEPENDENCIES = ['frontend'] diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 3aa1e3643c6fa7..26628d7fe6255c 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -5,6 +5,7 @@ attrs==18.1.0 certifi>=2018.04.16 jinja2>=2.10 PyJWT==1.6.4 +cryptography==2.3.1 pip>=8.0.3 pytz>=2018.04 pyyaml>=3.13,<4 diff --git a/requirements_all.txt b/requirements_all.txt index 3f50e50d19afc7..5289db61f6282c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -6,6 +6,7 @@ attrs==18.1.0 certifi>=2018.04.16 jinja2>=2.10 PyJWT==1.6.4 +cryptography==2.3.1 pip>=8.0.3 pytz>=2018.04 pyyaml>=3.13,<4 @@ -39,9 +40,6 @@ Mastodon.py==1.3.1 # homeassistant.components.isy994 PyISY==1.1.0 -# homeassistant.components.notify.html5 -PyJWT==1.6.0 - # homeassistant.components.sensor.mvglive PyMVGLive==1.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ffc55c23210236..4115fcfcb3f030 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -21,9 +21,6 @@ requests_mock==1.5.2 # homeassistant.components.homekit HAP-python==2.2.2 -# homeassistant.components.notify.html5 -PyJWT==1.6.0 - # homeassistant.components.sensor.rmvtransport PyRMVtransport==0.0.7 diff --git a/setup.py b/setup.py index bd1e70aa8ae6f1..7484dc286e62ee 100755 --- a/setup.py +++ b/setup.py @@ -39,6 +39,8 @@ 'certifi>=2018.04.16', 'jinja2>=2.10', 'PyJWT==1.6.4', + # PyJWT has loose dependency. We want the latest one. + 'cryptography==2.3.1', 'pip>=8.0.3', 'pytz>=2018.04', 'pyyaml>=3.13,<4', From e64e84ad7af2bf5578e08977732a576e4f9a48e5 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 14 Aug 2018 22:06:57 +0200 Subject: [PATCH 104/117] Bumped version to 0.76.0b2 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 52175c2b4e9203..200b58461b412f 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 76 -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 4035880003243abcb6d4a9e3623c3f3a1b8d9773 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 15 Aug 2018 10:50:11 +0200 Subject: [PATCH 105/117] Update translations --- .../components/deconz/.translations/de.json | 3 ++- .../homematicip_cloud/.translations/de.json | 4 +++- .../homematicip_cloud/.translations/no.json | 2 +- .../homematicip_cloud/.translations/pt.json | 2 +- homeassistant/components/hue/.translations/de.json | 2 +- homeassistant/components/hue/.translations/ro.json | 4 ++++ homeassistant/components/nest/.translations/de.json | 2 ++ .../components/sensor/.translations/moon.ar.json | 6 ++++++ .../components/sensor/.translations/moon.ca.json | 12 ++++++++++++ .../components/sensor/.translations/moon.de.json | 12 ++++++++++++ .../components/sensor/.translations/moon.en.json | 12 ++++++++++++ .../components/sensor/.translations/moon.es-419.json | 12 ++++++++++++ .../components/sensor/.translations/moon.fr.json | 12 ++++++++++++ .../components/sensor/.translations/moon.ko.json | 12 ++++++++++++ .../components/sensor/.translations/moon.nl.json | 12 ++++++++++++ .../components/sensor/.translations/moon.no.json | 12 ++++++++++++ .../components/sensor/.translations/moon.ru.json | 12 ++++++++++++ .../components/sensor/.translations/moon.sl.json | 12 ++++++++++++ .../sensor/.translations/moon.zh-Hans.json | 12 ++++++++++++ .../sensor/.translations/moon.zh-Hant.json | 12 ++++++++++++ 20 files changed, 164 insertions(+), 5 deletions(-) create mode 100644 homeassistant/components/sensor/.translations/moon.ar.json create mode 100644 homeassistant/components/sensor/.translations/moon.ca.json create mode 100644 homeassistant/components/sensor/.translations/moon.de.json create mode 100644 homeassistant/components/sensor/.translations/moon.en.json create mode 100644 homeassistant/components/sensor/.translations/moon.es-419.json create mode 100644 homeassistant/components/sensor/.translations/moon.fr.json create mode 100644 homeassistant/components/sensor/.translations/moon.ko.json create mode 100644 homeassistant/components/sensor/.translations/moon.nl.json create mode 100644 homeassistant/components/sensor/.translations/moon.no.json create mode 100644 homeassistant/components/sensor/.translations/moon.ru.json create mode 100644 homeassistant/components/sensor/.translations/moon.sl.json create mode 100644 homeassistant/components/sensor/.translations/moon.zh-Hans.json create mode 100644 homeassistant/components/sensor/.translations/moon.zh-Hant.json diff --git a/homeassistant/components/deconz/.translations/de.json b/homeassistant/components/deconz/.translations/de.json index b09b7e15b31a61..51b496906a2a08 100644 --- a/homeassistant/components/deconz/.translations/de.json +++ b/homeassistant/components/deconz/.translations/de.json @@ -24,7 +24,8 @@ "data": { "allow_clip_sensor": "Import virtueller Sensoren zulassen", "allow_deconz_groups": "Import von deCONZ-Gruppen zulassen" - } + }, + "title": "Weitere Konfigurationsoptionen f\u00fcr deCONZ" } }, "title": "deCONZ Zigbee Gateway" diff --git a/homeassistant/components/homematicip_cloud/.translations/de.json b/homeassistant/components/homematicip_cloud/.translations/de.json index 8e4130a32511d4..61a9bd6eb404bc 100644 --- a/homeassistant/components/homematicip_cloud/.translations/de.json +++ b/homeassistant/components/homematicip_cloud/.translations/de.json @@ -17,9 +17,11 @@ "hapid": "Accesspoint ID (SGTIN)", "name": "Name (optional, wird als Pr\u00e4fix f\u00fcr alle Ger\u00e4te verwendet)", "pin": "PIN Code (optional)" - } + }, + "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)", "title": "Verkn\u00fcpfe den Accesspoint" } }, diff --git a/homeassistant/components/homematicip_cloud/.translations/no.json b/homeassistant/components/homematicip_cloud/.translations/no.json index 7e164abd3bb272..650c921af31e1b 100644 --- a/homeassistant/components/homematicip_cloud/.translations/no.json +++ b/homeassistant/components/homematicip_cloud/.translations/no.json @@ -22,7 +22,7 @@ }, "link": { "description": "Trykk p\u00e5 den bl\u00e5 knappen p\u00e5 tilgangspunktet og send knappen for \u00e5 registrere HomematicIP med Home Assistant. \n\n![Plassering av knapp p\u00e5 bridge](/static/images/config_flows/config_homematicip_cloud.png)", - "title": "Link Tilgangspunkt" + "title": "Link tilgangspunkt" } }, "title": "HomematicIP Sky" diff --git a/homeassistant/components/homematicip_cloud/.translations/pt.json b/homeassistant/components/homematicip_cloud/.translations/pt.json index ef742e2ce5ed06..2266e83ac440a0 100644 --- a/homeassistant/components/homematicip_cloud/.translations/pt.json +++ b/homeassistant/components/homematicip_cloud/.translations/pt.json @@ -21,7 +21,7 @@ "title": "Escolher ponto de acesso HomematicIP" }, "link": { - "description": "Pressione o bot\u00e3o azul no accesspoint e o bot\u00e3o enviar para registrar HomematicIP com Home Assistant.\n\n! [Localiza\u00e7\u00e3o do bot\u00e3o na ponte] (/ static/images/config_flows/config_homematicip_cloud.png)", + "description": "Pressione o bot\u00e3o azul no ponto de acesso e o bot\u00e3o enviar para registrar HomematicIP com o Home Assistant.\n\n![Localiza\u00e7\u00e3o do bot\u00e3o na ponte](/ static/images/config_flows/config_homematicip_cloud.png)", "title": "Associar ponto de acesso" } }, diff --git a/homeassistant/components/hue/.translations/de.json b/homeassistant/components/hue/.translations/de.json index dc0968dc88acbb..a0bd50d8514dfc 100644 --- a/homeassistant/components/hue/.translations/de.json +++ b/homeassistant/components/hue/.translations/de.json @@ -24,6 +24,6 @@ "title": "Hub verbinden" } }, - "title": "" + "title": "Philips Hue" } } \ No newline at end of file diff --git a/homeassistant/components/hue/.translations/ro.json b/homeassistant/components/hue/.translations/ro.json index 91541edcc7d7ff..69cee1198d3e0f 100644 --- a/homeassistant/components/hue/.translations/ro.json +++ b/homeassistant/components/hue/.translations/ro.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "all_configured": "Toate pun\u021bile Philips Hue sunt deja configurate", + "discover_timeout": "Imposibil de descoperit podurile Hue" + }, "error": { "linking": "A ap\u0103rut o eroare de leg\u0103tur\u0103 necunoscut\u0103.", "register_failed": "Nu a reu\u0219it \u00eenregistrarea, \u00eencerca\u021bi din nou" diff --git a/homeassistant/components/nest/.translations/de.json b/homeassistant/components/nest/.translations/de.json index 32c72ef7d96874..86b50ab3c10323 100644 --- a/homeassistant/components/nest/.translations/de.json +++ b/homeassistant/components/nest/.translations/de.json @@ -2,6 +2,8 @@ "config": { "abort": { "already_setup": "Sie k\u00f6nnen 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/)." }, "error": { diff --git a/homeassistant/components/sensor/.translations/moon.ar.json b/homeassistant/components/sensor/.translations/moon.ar.json new file mode 100644 index 00000000000000..94af741f5f4de7 --- /dev/null +++ b/homeassistant/components/sensor/.translations/moon.ar.json @@ -0,0 +1,6 @@ +{ + "state": { + "first_quarter": "\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u0623\u0648\u0644", + "full_moon": "\u0627\u0644\u0642\u0645\u0631 \u0627\u0644\u0643\u0627\u0645\u0644" + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/moon.ca.json b/homeassistant/components/sensor/.translations/moon.ca.json new file mode 100644 index 00000000000000..56eaf8d3b4c8fe --- /dev/null +++ b/homeassistant/components/sensor/.translations/moon.ca.json @@ -0,0 +1,12 @@ +{ + "state": { + "first_quarter": "Quart creixent", + "full_moon": "Lluna plena", + "last_quarter": "Quart minvant", + "new_moon": "Lluna nova", + "waning_crescent": "Lluna vella minvant", + "waning_gibbous": "Gibosa minvant", + "waxing_crescent": "Lluna nova visible", + "waxing_gibbous": "Gibosa creixent" + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/moon.de.json b/homeassistant/components/sensor/.translations/moon.de.json new file mode 100644 index 00000000000000..aebca53ec4dcf7 --- /dev/null +++ b/homeassistant/components/sensor/.translations/moon.de.json @@ -0,0 +1,12 @@ +{ + "state": { + "first_quarter": "Erstes Viertel", + "full_moon": "Vollmond", + "last_quarter": "Letztes Viertel", + "new_moon": "Neumond", + "waning_crescent": "Abnehmende Sichel", + "waning_gibbous": "Drittes Viertel", + "waxing_crescent": " Zunehmende Sichel", + "waxing_gibbous": "Zweites Viertel" + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/moon.en.json b/homeassistant/components/sensor/.translations/moon.en.json new file mode 100644 index 00000000000000..587b9496114118 --- /dev/null +++ b/homeassistant/components/sensor/.translations/moon.en.json @@ -0,0 +1,12 @@ +{ + "state": { + "first_quarter": "First quarter", + "full_moon": "Full moon", + "last_quarter": "Last quarter", + "new_moon": "New moon", + "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/sensor/.translations/moon.es-419.json b/homeassistant/components/sensor/.translations/moon.es-419.json new file mode 100644 index 00000000000000..71cfab736cb6be --- /dev/null +++ b/homeassistant/components/sensor/.translations/moon.es-419.json @@ -0,0 +1,12 @@ +{ + "state": { + "first_quarter": "Cuarto creciente", + "full_moon": "Luna llena", + "last_quarter": "Cuarto menguante", + "new_moon": "Luna nueva", + "waning_crescent": "Luna menguante", + "waning_gibbous": "Luna menguante gibosa", + "waxing_crescent": "Luna creciente", + "waxing_gibbous": "Luna creciente gibosa" + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/moon.fr.json b/homeassistant/components/sensor/.translations/moon.fr.json new file mode 100644 index 00000000000000..fac2b654a4664d --- /dev/null +++ b/homeassistant/components/sensor/.translations/moon.fr.json @@ -0,0 +1,12 @@ +{ + "state": { + "first_quarter": "Premier quartier", + "full_moon": "Pleine lune", + "last_quarter": "Dernier quartier", + "new_moon": "Nouvelle lune", + "waning_crescent": "Dernier croissant", + "waning_gibbous": "Gibbeuse d\u00e9croissante", + "waxing_crescent": "Premier croissant", + "waxing_gibbous": "Gibbeuse croissante" + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/moon.ko.json b/homeassistant/components/sensor/.translations/moon.ko.json new file mode 100644 index 00000000000000..7e62250b89224a --- /dev/null +++ b/homeassistant/components/sensor/.translations/moon.ko.json @@ -0,0 +1,12 @@ +{ + "state": { + "first_quarter": "\ubc18\ub2ec(\ucc28\uc624\ub974\ub294)", + "full_moon": "\ubcf4\ub984\ub2ec", + "last_quarter": "\ubc18\ub2ec(\uc904\uc5b4\ub4dc\ub294)", + "new_moon": "\uc0ad\uc6d4", + "waning_crescent": "\uadf8\ubbd0\ub2ec", + "waning_gibbous": "\ud558\ud604\ub2ec", + "waxing_crescent": "\ucd08\uc2b9\ub2ec", + "waxing_gibbous": "\uc0c1\ud604\ub2ec" + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/moon.nl.json b/homeassistant/components/sensor/.translations/moon.nl.json new file mode 100644 index 00000000000000..5e78d429b9f079 --- /dev/null +++ b/homeassistant/components/sensor/.translations/moon.nl.json @@ -0,0 +1,12 @@ +{ + "state": { + "first_quarter": "Eerste kwartier", + "full_moon": "Volle maan", + "last_quarter": "Laatste kwartier", + "new_moon": "Nieuwe maan", + "waning_crescent": "Krimpende, sikkelvormige maan", + "waning_gibbous": "Krimpende, vooruitspringende maan", + "waxing_crescent": "Wassende, sikkelvormige maan", + "waxing_gibbous": "Wassende, vooruitspringende maan" + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/moon.no.json b/homeassistant/components/sensor/.translations/moon.no.json new file mode 100644 index 00000000000000..104412c90babfd --- /dev/null +++ b/homeassistant/components/sensor/.translations/moon.no.json @@ -0,0 +1,12 @@ +{ + "state": { + "first_quarter": "F\u00f8rste kvartdel", + "full_moon": "Fullm\u00e5ne", + "last_quarter": "Siste kvartdel", + "new_moon": "Nym\u00e5ne", + "waning_crescent": "Minkende halvm\u00e5ne", + "waning_gibbous": "Minkende trekvartm\u00e5ne", + "waxing_crescent": "Voksende halvm\u00e5ne", + "waxing_gibbous": "Voksende trekvartm\u00e5ne" + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/moon.ru.json b/homeassistant/components/sensor/.translations/moon.ru.json new file mode 100644 index 00000000000000..6db932a1aed0ae --- /dev/null +++ b/homeassistant/components/sensor/.translations/moon.ru.json @@ -0,0 +1,12 @@ +{ + "state": { + "first_quarter": "\u041f\u0435\u0440\u0432\u0430\u044f \u0447\u0435\u0442\u0432\u0435\u0440\u0442\u044c", + "full_moon": "\u041f\u043e\u043b\u043d\u043e\u043b\u0443\u043d\u0438\u0435", + "last_quarter": "\u041f\u043e\u0441\u043b\u0435\u0434\u043d\u044f\u044f \u0447\u0435\u0442\u0432\u0435\u0440\u0442\u044c", + "new_moon": "\u041d\u043e\u0432\u043e\u043b\u0443\u043d\u0438\u0435", + "waning_crescent": "\u0421\u0442\u0430\u0440\u0430\u044f \u043b\u0443\u043d\u0430", + "waning_gibbous": "\u0423\u0431\u044b\u0432\u0430\u044e\u0449\u0430\u044f \u043b\u0443\u043d\u0430", + "waxing_crescent": "\u041c\u043e\u043b\u043e\u0434\u0430\u044f \u043b\u0443\u043d\u0430", + "waxing_gibbous": "\u041f\u0440\u0438\u0431\u044b\u0432\u0430\u044e\u0449\u0430\u044f \u043b\u0443\u043d\u0430" + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/moon.sl.json b/homeassistant/components/sensor/.translations/moon.sl.json new file mode 100644 index 00000000000000..41e873e4defd80 --- /dev/null +++ b/homeassistant/components/sensor/.translations/moon.sl.json @@ -0,0 +1,12 @@ +{ + "state": { + "first_quarter": "Prvi krajec", + "full_moon": "Polna luna", + "last_quarter": "Zadnji krajec", + "new_moon": "Mlaj", + "waning_crescent": "Zadnji izbo\u010dec", + "waning_gibbous": "Zadnji srpec", + "waxing_crescent": " Prvi izbo\u010dec", + "waxing_gibbous": "Prvi srpec" + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/moon.zh-Hans.json b/homeassistant/components/sensor/.translations/moon.zh-Hans.json new file mode 100644 index 00000000000000..22ab0d49f62d06 --- /dev/null +++ b/homeassistant/components/sensor/.translations/moon.zh-Hans.json @@ -0,0 +1,12 @@ +{ + "state": { + "first_quarter": "\u4e0a\u5f26\u6708", + "full_moon": "\u6ee1\u6708", + "last_quarter": "\u4e0b\u5f26\u6708", + "new_moon": "\u65b0\u6708", + "waning_crescent": "\u6b8b\u6708", + "waning_gibbous": "\u4e8f\u51f8\u6708", + "waxing_crescent": "\u5ce8\u7709\u6708", + "waxing_gibbous": "\u76c8\u51f8\u6708" + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/moon.zh-Hant.json b/homeassistant/components/sensor/.translations/moon.zh-Hant.json new file mode 100644 index 00000000000000..9cf4aad011e04e --- /dev/null +++ b/homeassistant/components/sensor/.translations/moon.zh-Hant.json @@ -0,0 +1,12 @@ +{ + "state": { + "first_quarter": "\u4e0a\u5f26\u6708", + "full_moon": "\u6eff\u6708", + "last_quarter": "\u4e0b\u5f26\u6708", + "new_moon": "\u65b0\u6708", + "waning_crescent": "\u6b98\u6708", + "waning_gibbous": "\u8667\u51f8\u6708", + "waxing_crescent": "\u86fe\u7709\u6708", + "waxing_gibbous": "\u76c8\u51f8\u6708" + } +} \ No newline at end of file From 2306d14b5dd4d5f115c8e4b7ac233287edb66c7f Mon Sep 17 00:00:00 2001 From: Jason Hu Date: Tue, 14 Aug 2018 23:09:19 -0700 Subject: [PATCH 106/117] Teak mqtt error message for 0.76 release (#15983) --- homeassistant/components/mqtt/__init__.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 70d4d7aa5d74d4..19bacbc8d4c2ce 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -354,11 +354,11 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: if (conf.get(CONF_PASSWORD) is None and config.get('http') is not None and config['http'].get('api_password') is not None): - _LOGGER.error("Starting from 0.77, embedded MQTT broker doesn't" - " use api_password as default password any more." - " Please set password configuration. See https://" - "home-assistant.io/docs/mqtt/broker#embedded-broker" - " for details") + _LOGGER.error( + "Starting from release 0.76, the embedded MQTT broker does not" + " use api_password as default password anymore. Please set" + " password configuration. See https://home-assistant.io/docs/" + "mqtt/broker#embedded-broker for details") return False broker_config = await _async_setup_server(hass, config) From f8051a56987dc6569640c196406d40b52c863bc6 Mon Sep 17 00:00:00 2001 From: Jason Hu Date: Wed, 15 Aug 2018 00:56:05 -0700 Subject: [PATCH 107/117] Fix 0.76 beta2 hassio token issue (#15987) --- homeassistant/components/hassio/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index 13c486533d9975..e0356017e3ebbe 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -178,7 +178,7 @@ def async_setup(hass, config): refresh_token = None if 'hassio_user' in data: user = yield from hass.auth.async_get_user(data['hassio_user']) - if user: + if user and user.refresh_tokens: refresh_token = list(user.refresh_tokens.values())[0] if refresh_token is None: From 6da0ae4d231052aad4c5f3e1677e3df050c73fdc Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 15 Aug 2018 10:55:03 +0200 Subject: [PATCH 108/117] Bumped version to 0.76.0b3 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 200b58461b412f..5e7d6f9585ba5d 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 76 -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 11eb29f520191d20a9b777d0f30c434d77bd0677 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 16 Aug 2018 14:21:43 +0200 Subject: [PATCH 109/117] Bump frontend to 20180816.0 --- homeassistant/components/frontend/__init__.py | 2 +- homeassistant/components/map.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 41cfdd3edd80ce..c3c742f43b17c7 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -26,7 +26,7 @@ from homeassistant.loader import bind_hass from homeassistant.util.yaml import load_yaml -REQUIREMENTS = ['home-assistant-frontend==20180813.0'] +REQUIREMENTS = ['home-assistant-frontend==20180816.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log', diff --git a/homeassistant/components/map.py b/homeassistant/components/map.py index 30cb00af69ea98..c0184239a1aefe 100644 --- a/homeassistant/components/map.py +++ b/homeassistant/components/map.py @@ -10,5 +10,5 @@ async def async_setup(hass, config): """Register the built-in map panel.""" await hass.components.frontend.async_register_built_in_panel( - 'map', 'map', 'mdi:account-location') + 'map', 'map', 'hass:account-location') return True diff --git a/requirements_all.txt b/requirements_all.txt index 5289db61f6282c..475f19f3dd0908 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -432,7 +432,7 @@ hole==0.3.0 holidays==0.9.6 # homeassistant.components.frontend -home-assistant-frontend==20180813.0 +home-assistant-frontend==20180816.0 # homeassistant.components.homekit_controller # homekit==0.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4115fcfcb3f030..0958b6d7280958 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.2 holidays==0.9.6 # homeassistant.components.frontend -home-assistant-frontend==20180813.0 +home-assistant-frontend==20180816.0 # homeassistant.components.homematicip_cloud homematicip==0.9.8 From d540a084dd07e01b7170e1be1843f87607d61526 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Thu, 16 Aug 2018 14:19:42 +0200 Subject: [PATCH 110/117] Fix mysensors connection task blocking setup (#15938) * Fix mysensors connection task blocking setup * Schedule the connection task without having the core track the task to avoid blocking setup. * Cancel the connection task, if not cancelled already, when home assistant stops. * Use done instead of cancelled --- homeassistant/components/mysensors/gateway.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/mysensors/gateway.py b/homeassistant/components/mysensors/gateway.py index 8c80604d1881ef..88725e67940d68 100644 --- a/homeassistant/components/mysensors/gateway.py +++ b/homeassistant/components/mysensors/gateway.py @@ -186,12 +186,16 @@ def _discover_mysensors_platform(hass, platform, new_devices): async def _gw_start(hass, gateway): """Start the gateway.""" + # Don't use hass.async_create_task to avoid holding up setup indefinitely. + connect_task = hass.loop.create_task(gateway.start()) + @callback def gw_stop(event): """Trigger to stop the gateway.""" hass.async_add_job(gateway.stop()) + if not connect_task.done(): + connect_task.cancel() - await gateway.start() hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, gw_stop) if gateway.device == 'mqtt': # Gatways connected via mqtt doesn't send gateway ready message. From 2469bc7e2ed6b6b64e5fc8071e631b0c726f3b65 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 16 Aug 2018 13:46:43 +0200 Subject: [PATCH 111/117] Fix Nest async from sync (#15997) --- homeassistant/components/nest/__init__.py | 43 +++++++++++++---------- 1 file changed, 25 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/nest/__init__.py b/homeassistant/components/nest/__init__.py index de9783ba931d26..d25b94bbc17682 100644 --- a/homeassistant/components/nest/__init__.py +++ b/homeassistant/components/nest/__init__.py @@ -4,10 +4,10 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/nest/ """ -from concurrent.futures import ThreadPoolExecutor import logging import socket from datetime import datetime, timedelta +import threading import voluptuous as vol @@ -16,8 +16,9 @@ CONF_STRUCTURE, CONF_FILENAME, CONF_BINARY_SENSORS, CONF_SENSORS, CONF_MONITORED_CONDITIONS, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) +from homeassistant.core import callback from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.dispatcher import async_dispatcher_send, \ +from homeassistant.helpers.dispatcher import dispatcher_send, \ async_dispatcher_connect from homeassistant.helpers.entity import Entity @@ -71,24 +72,25 @@ }, extra=vol.ALLOW_EXTRA) -async def async_nest_update_event_broker(hass, nest): +def nest_update_event_broker(hass, nest): """ Dispatch SIGNAL_NEST_UPDATE to devices when nest stream API received data. - nest.update_event.wait will block the thread in most of time, - so specific an executor to save default thread pool. + Runs in its own thread. """ _LOGGER.debug("listening nest.update_event") - with ThreadPoolExecutor(max_workers=1) as executor: - while True: - await hass.loop.run_in_executor(executor, nest.update_event.wait) - if hass.is_running: - nest.update_event.clear() - _LOGGER.debug("dispatching nest data update") - async_dispatcher_send(hass, SIGNAL_NEST_UPDATE) - else: - _LOGGER.debug("stop listening nest.update_event") - return + + while hass.is_running: + nest.update_event.wait() + + if not hass.is_running: + break + + nest.update_event.clear() + _LOGGER.debug("dispatching nest data update") + dispatcher_send(hass, SIGNAL_NEST_UPDATE) + + _LOGGER.debug("stop listening nest.update_event") async def async_setup(hass, config): @@ -167,16 +169,21 @@ def set_mode(service): hass.services.async_register( DOMAIN, 'set_mode', set_mode, schema=AWAY_SCHEMA) + @callback def start_up(event): """Start Nest update event listener.""" - hass.async_add_job(async_nest_update_event_broker, hass, nest) + threading.Thread( + name='Nest update listener', + target=nest_update_event_broker, + args=(hass, nest) + ).start() hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, start_up) + @callback def shut_down(event): """Stop Nest update event listener.""" - if nest: - nest.update_event.set() + nest.update_event.set() hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, shut_down) From 5eccfc2604ff72580a9bec6a0bab789ff4a09b9f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 16 Aug 2018 14:26:52 +0200 Subject: [PATCH 112/117] Bumped version to 0.76.0b4 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 5e7d6f9585ba5d..8914a6ba76c646 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 76 -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 e41ce1d6ecb8c2b4497a1f6e1eda3b13d596fa88 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 16 Aug 2018 22:15:39 +0200 Subject: [PATCH 113/117] Bump frontend to 20180816.1 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index c3c742f43b17c7..40fb6056684f91 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -26,7 +26,7 @@ from homeassistant.loader import bind_hass from homeassistant.util.yaml import load_yaml -REQUIREMENTS = ['home-assistant-frontend==20180816.0'] +REQUIREMENTS = ['home-assistant-frontend==20180816.1'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log', diff --git a/requirements_all.txt b/requirements_all.txt index 475f19f3dd0908..aad64d205f34f4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -432,7 +432,7 @@ hole==0.3.0 holidays==0.9.6 # homeassistant.components.frontend -home-assistant-frontend==20180816.0 +home-assistant-frontend==20180816.1 # homeassistant.components.homekit_controller # homekit==0.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0958b6d7280958..8b1534940252a6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.2 holidays==0.9.6 # homeassistant.components.frontend -home-assistant-frontend==20180816.0 +home-assistant-frontend==20180816.1 # homeassistant.components.homematicip_cloud homematicip==0.9.8 From 061859cc4decd3745724fc35222d71af3c180871 Mon Sep 17 00:00:00 2001 From: Steven Looman Date: Thu, 16 Aug 2018 22:42:11 +0200 Subject: [PATCH 114/117] Fix message "Updating dlna_dmr media_player took longer than ..." (#16005) --- 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 9b6beb833412f8..c40e3ed0ca9d30 100644 --- a/homeassistant/components/media_player/dlna_dmr.py +++ b/homeassistant/components/media_player/dlna_dmr.py @@ -35,7 +35,7 @@ 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 aad64d205f34f4..d65ef4fce18143 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 92e26495da9dad9359fcef426e37b5f510f30d8e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 16 Aug 2018 22:41:44 +0200 Subject: [PATCH 115/117] Disable the DLNA component discovery (#16006) --- homeassistant/components/discovery.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/discovery.py b/homeassistant/components/discovery.py index b400d1d88855d5..41cf3791256d7a 100644 --- a/homeassistant/components/discovery.py +++ b/homeassistant/components/discovery.py @@ -85,11 +85,11 @@ 'volumio': ('media_player', 'volumio'), 'nanoleaf_aurora': ('light', 'nanoleaf_aurora'), 'freebox': ('device_tracker', 'freebox'), - 'dlna_dmr': ('media_player', 'dlna_dmr'), } OPTIONAL_SERVICE_HANDLERS = { SERVICE_HOMEKIT: ('homekit_controller', None), + 'dlna_dmr': ('media_player', 'dlna_dmr'), } CONF_IGNORE = 'ignore' From c09e7e620f092fb400e0d6e3e39feb85e2e4b44c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 16 Aug 2018 23:02:34 +0200 Subject: [PATCH 116/117] Bumped version to 0.76.0b5 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 8914a6ba76c646..7a9dbaff04205c 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 76 -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 e4425e6a37eb8a311221de5942f4d8ef20a89443 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 17 Aug 2018 17:23:20 +0200 Subject: [PATCH 117/117] Version 0.76.0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 7a9dbaff04205c..5a481e0a8c10cd 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 76 -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)