From c5228cbc64bd8bc0004bf2e8f823edad8aa9f6aa Mon Sep 17 00:00:00 2001 From: PhracturedBlue Date: Thu, 8 Mar 2018 18:23:52 -0800 Subject: [PATCH 001/220] Add camera proxy (#12006) * Add camera proxy * Fix additional tox linting issues * Trivial cleanup * update to new async/await methods rather than decorators. Other minor fixes from code review --- homeassistant/components/camera/proxy.py | 262 +++++++++++++++++++++++ requirements_all.txt | 3 + 2 files changed, 265 insertions(+) create mode 100644 homeassistant/components/camera/proxy.py diff --git a/homeassistant/components/camera/proxy.py b/homeassistant/components/camera/proxy.py new file mode 100644 index 00000000000..56b9db5c0ec --- /dev/null +++ b/homeassistant/components/camera/proxy.py @@ -0,0 +1,262 @@ +""" +Proxy camera platform that enables image processing of camera data. + +For more details about this platform, please refer to the documentation +https://home-assistant.io/components/proxy +""" +import logging +import asyncio +import aiohttp +import async_timeout + +import voluptuous as vol + +from homeassistant.util.async import run_coroutine_threadsafe +from homeassistant.helpers import config_validation as cv + +import homeassistant.util.dt as dt_util +from homeassistant.const import ( + CONF_NAME, CONF_ENTITY_ID, HTTP_HEADER_HA_AUTH) +from homeassistant.components.camera import ( + PLATFORM_SCHEMA, Camera) +from homeassistant.helpers.aiohttp_client import ( + async_get_clientsession, async_aiohttp_proxy_web) + +REQUIREMENTS = ['pillow==5.0.0'] + +_LOGGER = logging.getLogger(__name__) + +CONF_MAX_IMAGE_WIDTH = "max_image_width" +CONF_IMAGE_QUALITY = "image_quality" +CONF_IMAGE_REFRESH_RATE = "image_refresh_rate" +CONF_FORCE_RESIZE = "force_resize" +CONF_MAX_STREAM_WIDTH = "max_stream_width" +CONF_STREAM_QUALITY = "stream_quality" +CONF_CACHE_IMAGES = "cache_images" + +DEFAULT_BASENAME = "Camera Proxy" +DEFAULT_QUALITY = 75 + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_ENTITY_ID): cv.entity_id, + vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_MAX_IMAGE_WIDTH): int, + vol.Optional(CONF_IMAGE_QUALITY): int, + vol.Optional(CONF_IMAGE_REFRESH_RATE): float, + vol.Optional(CONF_FORCE_RESIZE, False): cv.boolean, + vol.Optional(CONF_CACHE_IMAGES, False): cv.boolean, + vol.Optional(CONF_MAX_STREAM_WIDTH): int, + vol.Optional(CONF_STREAM_QUALITY): int, +}) + + +async def async_setup_platform(hass, config, async_add_devices, + discovery_info=None): + """Set up the Proxy camera platform.""" + async_add_devices([ProxyCamera(hass, config)]) + + +async def _read_frame(req): + """Read a single frame from an MJPEG stream.""" + # based on https://gist.github.com/russss/1143799 + import cgi + # Read in HTTP headers: + stream = req.content + # multipart/x-mixed-replace; boundary=--frameboundary + _mimetype, options = cgi.parse_header(req.headers['content-type']) + boundary = options.get('boundary').encode('utf-8') + if not boundary: + _LOGGER.error("Malformed MJPEG missing boundary") + raise Exception("Can't find content-type") + + line = await stream.readline() + # Seek ahead to the first chunk + while line.strip() != boundary: + line = await stream.readline() + # Read in chunk headers + while line.strip() != b'': + parts = line.split(b':') + if len(parts) > 1 and parts[0].lower() == b'content-length': + # Grab chunk length + length = int(parts[1].strip()) + line = await stream.readline() + image = await stream.read(length) + return image + + +def _resize_image(image, opts): + """Resize image.""" + from PIL import Image + import io + + if not opts: + return image + + quality = opts.quality or DEFAULT_QUALITY + new_width = opts.max_width + + img = Image.open(io.BytesIO(image)) + imgfmt = str(img.format) + if imgfmt != 'PNG' and imgfmt != 'JPEG': + _LOGGER.debug("Image is of unsupported type: %s", imgfmt) + return image + + (old_width, old_height) = img.size + old_size = len(image) + if old_width <= new_width: + if opts.quality is None: + _LOGGER.debug("Image is smaller-than / equal-to requested width") + return image + new_width = old_width + + scale = new_width / float(old_width) + new_height = int((float(old_height)*float(scale))) + + img = img.resize((new_width, new_height), Image.ANTIALIAS) + imgbuf = io.BytesIO() + img.save(imgbuf, "JPEG", optimize=True, quality=quality) + newimage = imgbuf.getvalue() + if not opts.force_resize and len(newimage) >= old_size: + _LOGGER.debug("Using original image(%d bytes) " + "because resized image (%d bytes) is not smaller", + old_size, len(newimage)) + return image + + _LOGGER.debug("Resized image " + "from (%dx%d - %d bytes) " + "to (%dx%d - %d bytes)", + old_width, old_height, old_size, + new_width, new_height, len(newimage)) + return newimage + + +class ImageOpts(): + """The representation of image options.""" + + def __init__(self, max_width, quality, force_resize): + """Initialize image options.""" + self.max_width = max_width + self.quality = quality + self.force_resize = force_resize + + def __bool__(self): + """Bool evalution rules.""" + return bool(self.max_width or self.quality) + + +class ProxyCamera(Camera): + """The representation of a Proxy camera.""" + + def __init__(self, hass, config): + """Initialize a proxy camera component.""" + super().__init__() + self.hass = hass + self._proxied_camera = config.get(CONF_ENTITY_ID) + self._name = ( + config.get(CONF_NAME) or + "{} - {}".format(DEFAULT_BASENAME, self._proxied_camera)) + self._image_opts = ImageOpts( + config.get(CONF_MAX_IMAGE_WIDTH), + config.get(CONF_IMAGE_QUALITY), + config.get(CONF_FORCE_RESIZE)) + + self._stream_opts = ImageOpts( + config.get(CONF_MAX_STREAM_WIDTH), + config.get(CONF_STREAM_QUALITY), + True) + + self._image_refresh_rate = config.get(CONF_IMAGE_REFRESH_RATE) + self._cache_images = bool( + config.get(CONF_IMAGE_REFRESH_RATE) + or config.get(CONF_CACHE_IMAGES)) + self._last_image_time = 0 + self._last_image = None + self._headers = ( + {HTTP_HEADER_HA_AUTH: self.hass.config.api.api_password} + if self.hass.config.api.api_password is not None + else None) + + def camera_image(self): + """Return camera image.""" + return run_coroutine_threadsafe( + self.async_camera_image(), self.hass.loop).result() + + async def async_camera_image(self): + """Return a still image response from the camera.""" + now = dt_util.utcnow() + + if (self._image_refresh_rate and + now < self._last_image_time + self._image_refresh_rate): + return self._last_image + + self._last_image_time = now + url = "{}/api/camera_proxy/{}".format( + self.hass.config.api.base_url, self._proxied_camera) + try: + websession = async_get_clientsession(self.hass) + with async_timeout.timeout(10, loop=self.hass.loop): + response = await websession.get(url, headers=self._headers) + image = await response.read() + except asyncio.TimeoutError: + _LOGGER.error("Timeout getting camera image") + return self._last_image + except aiohttp.ClientError as err: + _LOGGER.error("Error getting new camera image: %s", err) + return self._last_image + + image = await self.hass.async_add_job( + _resize_image, image, self._image_opts) + + if self._cache_images: + self._last_image = image + return image + + async def handle_async_mjpeg_stream(self, request): + """Generate an HTTP MJPEG stream from camera images.""" + websession = async_get_clientsession(self.hass) + url = "{}/api/camera_proxy_stream/{}".format( + self.hass.config.api.base_url, self._proxied_camera) + stream_coro = websession.get(url, headers=self._headers) + + if not self._stream_opts: + await async_aiohttp_proxy_web(self.hass, request, stream_coro) + return + + response = aiohttp.web.StreamResponse() + response.content_type = ('multipart/x-mixed-replace; ' + 'boundary=--frameboundary') + await response.prepare(request) + + def write(img_bytes): + """Write image to stream.""" + response.write(bytes( + '--frameboundary\r\n' + 'Content-Type: {}\r\n' + 'Content-Length: {}\r\n\r\n'.format( + self.content_type, len(img_bytes)), + 'utf-8') + img_bytes + b'\r\n') + + with async_timeout.timeout(10, loop=self.hass.loop): + req = await stream_coro + + try: + while True: + image = await _read_frame(req) + if not image: + break + image = await self.hass.async_add_job( + _resize_image, image, self._stream_opts) + write(image) + except asyncio.CancelledError: + _LOGGER.debug("Stream closed by frontend.") + req.close() + response = None + + finally: + if response is not None: + await response.write_eof() + + @property + def name(self): + """Return the name of this camera.""" + return self._name diff --git a/requirements_all.txt b/requirements_all.txt index e6eeb18fafc..b7af19f0d66 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -580,6 +580,9 @@ piglow==1.2.4 # homeassistant.components.pilight pilight==0.1.1 +# homeassistant.components.camera.proxy +pillow==5.0.0 + # homeassistant.components.dominos pizzapi==0.0.3 From 5e2296f2a45801bee07b5f64a641e700bc6c18e4 Mon Sep 17 00:00:00 2001 From: Steve Easley Date: Thu, 8 Mar 2018 22:31:52 -0500 Subject: [PATCH 002/220] Get zha switch and binary_sensor state on startup (#11672) * Get zha switch and binary_sensor state on startup * Removed unused var * Make zha switch report status * Use right method name * Formatting fix * Updates to match latest dev * PR feedback updates * Use async for cluster commands --- homeassistant/components/binary_sensor/zha.py | 33 ++++++++++---- homeassistant/components/switch/zha.py | 43 ++++++++++++++----- 2 files changed, 57 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/binary_sensor/zha.py b/homeassistant/components/binary_sensor/zha.py index de7896e595b..bf038a62465 100644 --- a/homeassistant/components/binary_sensor/zha.py +++ b/homeassistant/components/binary_sensor/zha.py @@ -4,7 +4,6 @@ Binary sensors on Zigbee Home Automation networks. For more details on this platform, please refer to the documentation at https://home-assistant.io/components/binary_sensor.zha/ """ -import asyncio import logging from homeassistant.components.binary_sensor import DOMAIN, BinarySensorDevice @@ -25,8 +24,8 @@ CLASS_MAPPING = { } -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_devices, discovery_info=None): +async def async_setup_platform(hass, config, async_add_devices, + discovery_info=None): """Set up the Zigbee Home Automation binary sensors.""" discovery_info = zha.get_discovery_info(hass, discovery_info) if discovery_info is None: @@ -39,19 +38,19 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): device_class = None cluster = in_clusters[IasZone.cluster_id] if discovery_info['new_join']: - yield from cluster.bind() + await cluster.bind() ieee = cluster.endpoint.device.application.ieee - yield from cluster.write_attributes({'cie_addr': ieee}) + await cluster.write_attributes({'cie_addr': ieee}) try: - zone_type = yield from cluster['zone_type'] + zone_type = await cluster['zone_type'] device_class = CLASS_MAPPING.get(zone_type, None) except Exception: # pylint: disable=broad-except # If we fail to read from the device, use a non-specific class pass sensor = BinarySensor(device_class, **discovery_info) - async_add_devices([sensor]) + async_add_devices([sensor], update_before_add=True) class BinarySensor(zha.Entity, BinarySensorDevice): @@ -66,6 +65,11 @@ class BinarySensor(zha.Entity, BinarySensorDevice): from zigpy.zcl.clusters.security import IasZone self._ias_zone_cluster = self._in_clusters[IasZone.cluster_id] + @property + def should_poll(self) -> bool: + """Let zha handle polling.""" + return False + @property def is_on(self) -> bool: """Return True if entity is on.""" @@ -83,7 +87,18 @@ class BinarySensor(zha.Entity, BinarySensorDevice): if command_id == 0: self._state = args[0] & 3 _LOGGER.debug("Updated alarm state: %s", self._state) - self.schedule_update_ha_state() + self.async_schedule_update_ha_state() elif command_id == 1: _LOGGER.debug("Enroll requested") - self.hass.add_job(self._ias_zone_cluster.enroll_response(0, 0)) + res = self._ias_zone_cluster.enroll_response(0, 0) + self.hass.async_add_job(res) + + async def async_update(self): + """Retrieve latest state.""" + from bellows.types.basic import uint16_t + + result = await zha.safe_read(self._endpoint.ias_zone, + ['zone_status']) + state = result.get('zone_status', self._state) + if isinstance(state, (int, uint16_t)): + self._state = result.get('zone_status', self._state) & 3 diff --git a/homeassistant/components/switch/zha.py b/homeassistant/components/switch/zha.py index c98db2e894e..7de9f1459b1 100644 --- a/homeassistant/components/switch/zha.py +++ b/homeassistant/components/switch/zha.py @@ -4,7 +4,6 @@ Switches on Zigbee Home Automation networks. For more details on this platform, please refer to the documentation at https://home-assistant.io/components/switch.zha/ """ -import asyncio import logging from homeassistant.components.switch import DOMAIN, SwitchDevice @@ -15,19 +14,39 @@ _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['zha'] -def setup_platform(hass, config, add_devices, discovery_info=None): - """Set up Zigbee Home Automation switches.""" +async def async_setup_platform(hass, config, async_add_devices, + discovery_info=None): + """Set up the Zigbee Home Automation switches.""" discovery_info = zha.get_discovery_info(hass, discovery_info) if discovery_info is None: return - add_devices([Switch(**discovery_info)]) + from zigpy.zcl.clusters.general import OnOff + in_clusters = discovery_info['in_clusters'] + cluster = in_clusters[OnOff.cluster_id] + await cluster.bind() + await cluster.configure_reporting(0, 0, 600, 1,) + + async_add_devices([Switch(**discovery_info)], update_before_add=True) class Switch(zha.Entity, SwitchDevice): """ZHA switch.""" _domain = DOMAIN + value_attribute = 0 + + def attribute_updated(self, attribute, value): + """Handle attribute update from device.""" + _LOGGER.debug("Attribute updated: %s %s %s", self, attribute, value) + if attribute == self.value_attribute: + self._state = value + self.async_schedule_update_ha_state() + + @property + def should_poll(self) -> bool: + """Let zha handle polling.""" + return False @property def is_on(self) -> bool: @@ -36,14 +55,18 @@ class Switch(zha.Entity, SwitchDevice): return False return bool(self._state) - @asyncio.coroutine - def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): """Turn the entity on.""" - yield from self._endpoint.on_off.on() + await self._endpoint.on_off.on() self._state = 1 - @asyncio.coroutine - def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs): """Turn the entity off.""" - yield from self._endpoint.on_off.off() + await self._endpoint.on_off.off() self._state = 0 + + async def async_update(self): + """Retrieve latest state.""" + result = await zha.safe_read(self._endpoint.on_off, + ['on_off']) + self._state = result.get('on_off', self._state) From 6734c966b33a23c5d72b42661048c2f245c47882 Mon Sep 17 00:00:00 2001 From: Johann Kellerman Date: Fri, 9 Mar 2018 05:34:24 +0200 Subject: [PATCH 003/220] check_config script evolution (#12792) * Initial async_check_ha_config_file * check_ha_config_file * Various fixes * feedback - return the config * move_to_check_config --- homeassistant/bootstrap.py | 11 +- homeassistant/config.py | 31 ++- homeassistant/scripts/check_config.py | 285 ++++++++++++++++---------- tests/scripts/test_check_config.py | 239 ++++++++++----------- tests/test_config.py | 4 +- 5 files changed, 316 insertions(+), 254 deletions(-) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 4971cbccc9c..2f093f061d9 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -112,18 +112,13 @@ def async_from_config_dict(config: Dict[str, Any], if not loader.PREPARED: yield from hass.async_add_job(loader.prepare, hass) + # Make a copy because we are mutating it. + config = OrderedDict(config) + # Merge packages conf_util.merge_packages_config( config, core_config.get(conf_util.CONF_PACKAGES, {})) - # Make a copy because we are mutating it. - # Use OrderedDict in case original one was one. - # Convert values to dictionaries if they are None - new_config = OrderedDict() - for key, value in config.items(): - new_config[key] = value or {} - config = new_config - hass.config_entries = config_entries.ConfigEntries(hass, config) yield from hass.config_entries.async_load() diff --git a/homeassistant/config.py b/homeassistant/config.py index 1c8ca10f8c6..5f2c6cf1625 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -41,9 +41,9 @@ VERSION_FILE = '.HA_VERSION' CONFIG_DIR_NAME = '.homeassistant' DATA_CUSTOMIZE = 'hass_customize' -FILE_MIGRATION = [ - ['ios.conf', '.ios.conf'], -] +FILE_MIGRATION = ( + ('ios.conf', '.ios.conf'), +) DEFAULT_CORE_CONFIG = ( # Tuples (attribute, default, auto detect property, description) @@ -304,6 +304,9 @@ def load_yaml_config_file(config_path): _LOGGER.error(msg) raise HomeAssistantError(msg) + # Convert values to dictionaries if they are None + for key, value in conf_dict.items(): + conf_dict[key] = value or {} return conf_dict @@ -345,14 +348,22 @@ def process_ha_config_upgrade(hass): @callback def async_log_exception(ex, domain, config, hass): + """Log an error for configuration validation. + + This method must be run in the event loop. + """ + if hass is not None: + async_notify_setup_error(hass, domain, True) + _LOGGER.error(_format_config_error(ex, domain, config)) + + +@callback +def _format_config_error(ex, domain, config): """Generate log exception for configuration validation. This method must be run in the event loop. """ message = "Invalid config for [{}]: ".format(domain) - if hass is not None: - async_notify_setup_error(hass, domain, True) - if 'extra keys not allowed' in ex.error_message: message += '[{}] is an invalid option for [{}]. Check: {}->{}.'\ .format(ex.path[-1], domain, domain, @@ -369,7 +380,7 @@ def async_log_exception(ex, domain, config, hass): message += ('Please check the docs at ' 'https://home-assistant.io/components/{}/'.format(domain)) - _LOGGER.error(message) + return message async def async_process_ha_core_config(hass, config): @@ -497,7 +508,7 @@ async def async_process_ha_core_config(hass, config): def _log_pkg_error(package, component, config, message): - """Log an error while merging.""" + """Log an error while merging packages.""" message = "Package {} setup failed. Component {} {}".format( package, component, message) @@ -523,7 +534,7 @@ def _identify_config_schema(module): return '', schema -def merge_packages_config(config, packages): +def merge_packages_config(config, packages, _log_pkg_error=_log_pkg_error): """Merge packages into the top-level configuration. Mutate config.""" # pylint: disable=too-many-nested-blocks PACKAGES_CONFIG_SCHEMA(packages) @@ -589,7 +600,7 @@ def merge_packages_config(config, packages): def async_process_component_config(hass, config, domain): """Check component configuration and return processed configuration. - Raise a vol.Invalid exception on error. + Returns None on error. This method must be run in the event loop. """ diff --git a/homeassistant/scripts/check_config.py b/homeassistant/scripts/check_config.py index ecbd7ca22eb..4e80b3c6536 100644 --- a/homeassistant/scripts/check_config.py +++ b/homeassistant/scripts/check_config.py @@ -1,17 +1,23 @@ -"""Script to ensure a configuration file exists.""" +"""Script to check the configuration file.""" import argparse import logging import os -from collections import OrderedDict +from collections import OrderedDict, namedtuple from glob import glob from platform import system from unittest.mock import patch +import attr from typing import Dict, List, Sequence +import voluptuous as vol -from homeassistant.core import callback -from homeassistant import bootstrap, loader, setup, config as config_util +from homeassistant import bootstrap, core, loader +from homeassistant.config import ( + get_default_config_dir, CONF_CORE, CORE_CONFIG_SCHEMA, + CONF_PACKAGES, merge_packages_config, _format_config_error, + find_config_file, load_yaml_config_file, get_component, + extract_domain_configs, config_per_platform, get_platform) import homeassistant.util.yaml as yaml from homeassistant.exceptions import HomeAssistantError @@ -24,35 +30,18 @@ _LOGGER = logging.getLogger(__name__) MOCKS = { 'load': ("homeassistant.util.yaml.load_yaml", yaml.load_yaml), 'load*': ("homeassistant.config.load_yaml", yaml.load_yaml), - 'get': ("homeassistant.loader.get_component", loader.get_component), 'secrets': ("homeassistant.util.yaml._secret_yaml", yaml._secret_yaml), - 'except': ("homeassistant.config.async_log_exception", - config_util.async_log_exception), - 'package_error': ("homeassistant.config._log_pkg_error", - config_util._log_pkg_error), - 'logger_exception': ("homeassistant.setup._LOGGER.error", - setup._LOGGER.error), - 'logger_exception_bootstrap': ("homeassistant.bootstrap._LOGGER.error", - bootstrap._LOGGER.error), } SILENCE = ( - 'homeassistant.bootstrap.async_enable_logging', # callback - 'homeassistant.bootstrap.clear_secret_cache', - 'homeassistant.bootstrap.async_register_signal_handling', # callback - 'homeassistant.config.process_ha_config_upgrade', + 'homeassistant.scripts.check_config.yaml.clear_secret_cache', ) + PATCHES = {} C_HEAD = 'bold' ERROR_STR = 'General Errors' -@callback -def mock_cb(*args): - """Callback that returns None.""" - return None - - def color(the_color, *args, reset=None): """Color helper.""" from colorlog.escape_codes import escape_codes, parse_colors @@ -74,11 +63,11 @@ def run(script_args: List) -> int: '--script', choices=['check_config']) parser.add_argument( '-c', '--config', - default=config_util.get_default_config_dir(), + default=get_default_config_dir(), help="Directory that contains the Home Assistant configuration") parser.add_argument( - '-i', '--info', - default=None, + '-i', '--info', nargs='?', + default=None, const='all', help="Show a portion of the config") parser.add_argument( '-f', '--files', @@ -89,21 +78,20 @@ def run(script_args: List) -> int: action='store_true', help="Show secret information") - args = parser.parse_args() + args, unknown = parser.parse_known_args() + if unknown: + print(color('red', "Unknown arguments:", ', '.join(unknown))) config_dir = os.path.join(os.getcwd(), args.config) - config_path = os.path.join(config_dir, 'configuration.yaml') - if not os.path.isfile(config_path): - print('Config does not exist:', config_path) - return 1 print(color('bold', "Testing configuration at", config_dir)) + res = check(config_dir, args.secrets) + domain_info = [] if args.info: domain_info = args.info.split(',') - res = check(config_path) if args.files: print(color(C_HEAD, 'yaml files'), '(used /', color('red', 'not used') + ')') @@ -158,59 +146,23 @@ def run(script_args: List) -> int: return len(res['except']) -def check(config_path): +def check(config_dir, secrets=False): """Perform a check by mocking hass load functions.""" - logging.getLogger('homeassistant.core').setLevel(logging.WARNING) - logging.getLogger('homeassistant.loader').setLevel(logging.WARNING) - logging.getLogger('homeassistant.setup').setLevel(logging.WARNING) - logging.getLogger('homeassistant.bootstrap').setLevel(logging.ERROR) - logging.getLogger('homeassistant.util.yaml').setLevel(logging.INFO) + logging.getLogger('homeassistant.loader').setLevel(logging.CRITICAL) res = { 'yaml_files': OrderedDict(), # yaml_files loaded 'secrets': OrderedDict(), # secret cache and secrets loaded 'except': OrderedDict(), # exceptions raised (with config) - 'components': OrderedDict(), # successful components - 'secret_cache': OrderedDict(), + 'components': None, # successful components + 'secret_cache': None, } # pylint: disable=unused-variable def mock_load(filename): - """Mock hass.util.load_yaml to save config files.""" + """Mock hass.util.load_yaml to save config file names.""" res['yaml_files'][filename] = True return MOCKS['load'][1](filename) - # pylint: disable=unused-variable - def mock_get(comp_name): - """Mock hass.loader.get_component to replace setup & setup_platform.""" - async def mock_async_setup(*args): - """Mock setup, only record the component name & config.""" - assert comp_name not in res['components'], \ - "Components should contain a list of platforms" - res['components'][comp_name] = args[1].get(comp_name) - return True - module = MOCKS['get'][1](comp_name) - - if module is None: - # Ensure list - msg = '{} not found: {}'.format( - 'Platform' if '.' in comp_name else 'Component', comp_name) - res['except'].setdefault(ERROR_STR, []).append(msg) - return None - - # Test if platform/component and overwrite setup - if '.' in comp_name: - module.async_setup_platform = mock_async_setup - - if hasattr(module, 'setup_platform'): - del module.setup_platform - else: - module.async_setup = mock_async_setup - - if hasattr(module, 'setup'): - del module.setup - - return module - # pylint: disable=unused-variable def mock_secrets(ldr, node): """Mock _get_secrets.""" @@ -221,37 +173,14 @@ def check(config_path): res['secrets'][node.value] = val return val - def mock_except(ex, domain, config, # pylint: disable=unused-variable - hass=None): - """Mock config.log_exception.""" - MOCKS['except'][1](ex, domain, config, hass) - res['except'][domain] = config.get(domain, config) - - def mock_package_error( # pylint: disable=unused-variable - package, component, config, message): - """Mock config_util._log_pkg_error.""" - MOCKS['package_error'][1](package, component, config, message) - - pkg_key = 'homeassistant.packages.{}'.format(package) - res['except'][pkg_key] = config.get('homeassistant', {}) \ - .get('packages', {}).get(package) - - def mock_logger_exception(msg, *params): - """Log logger.exceptions.""" - res['except'].setdefault(ERROR_STR, []).append(msg % params) - MOCKS['logger_exception'][1](msg, *params) - - def mock_logger_exception_bootstrap(msg, *params): - """Log logger.exceptions.""" - res['except'].setdefault(ERROR_STR, []).append(msg % params) - MOCKS['logger_exception_bootstrap'][1](msg, *params) - # Patches to skip functions for sil in SILENCE: - PATCHES[sil] = patch(sil, return_value=mock_cb()) + PATCHES[sil] = patch(sil) # Patches with local mock functions for key, val in MOCKS.items(): + if not secrets and key == 'secrets': + continue # The * in the key is removed to find the mock_function (side_effect) # This allows us to use one side_effect to patch multiple locations mock_function = locals()['mock_' + key.replace('*', '')] @@ -260,22 +189,42 @@ def check(config_path): # Start all patches for pat in PATCHES.values(): pat.start() - # Ensure !secrets point to the patched function - yaml.yaml.SafeLoader.add_constructor('!secret', yaml._secret_yaml) + + if secrets: + # Ensure !secrets point to the patched function + yaml.yaml.SafeLoader.add_constructor('!secret', yaml._secret_yaml) try: - with patch('homeassistant.util.logging.AsyncHandler._process'): - bootstrap.from_config_file(config_path, skip_pip=True) - res['secret_cache'] = dict(yaml.__SECRET_CACHE) + class HassConfig(): + """Hass object with config.""" + + def __init__(self, conf_dir): + """Init the config_dir.""" + self.config = core.Config() + self.config.config_dir = conf_dir + + loader.prepare(HassConfig(config_dir)) + + res['components'] = check_ha_config_file(config_dir) + + res['secret_cache'] = OrderedDict(yaml.__SECRET_CACHE) + + for err in res['components'].errors: + domain = err.domain or ERROR_STR + res['except'].setdefault(domain, []).append(err.message) + if err.config: + res['except'].setdefault(domain, []).append(err.config) + except Exception as err: # pylint: disable=broad-except print(color('red', 'Fatal error while loading config:'), str(err)) - res['except'].setdefault(ERROR_STR, []).append(err) + res['except'].setdefault(ERROR_STR, []).append(str(err)) finally: # Stop all patches for pat in PATCHES.values(): pat.stop() - # Ensure !secrets point to the original function - yaml.yaml.SafeLoader.add_constructor('!secret', yaml._secret_yaml) + if secrets: + # Ensure !secrets point to the original function + yaml.yaml.SafeLoader.add_constructor('!secret', yaml._secret_yaml) bootstrap.clear_secret_cache() return res @@ -317,3 +266,125 @@ def dump_dict(layer, indent_count=3, listi=False, **kwargs): dump_dict(i, indent_count + 2, True) else: print(' ', indent_str, i) + + +CheckConfigError = namedtuple( # pylint: disable=invalid-name + 'CheckConfigError', "message domain config") + + +@attr.s +class HomeAssistantConfig(OrderedDict): + """Configuration result with errors attribute.""" + + errors = attr.ib(default=attr.Factory(list)) + + def add_error(self, message, domain=None, config=None): + """Add a single error.""" + self.errors.append(CheckConfigError(str(message), domain, config)) + return self + + +def check_ha_config_file(config_dir): + """Check if Home Assistant configuration file is valid.""" + result = HomeAssistantConfig() + + def _pack_error(package, component, config, message): + """Handle errors from packages: _log_pkg_error.""" + message = "Package {} setup failed. Component {} {}".format( + package, component, message) + domain = 'homeassistant.packages.{}.{}'.format(package, component) + pack_config = core_config[CONF_PACKAGES].get(package, config) + result.add_error(message, domain, pack_config) + + def _comp_error(ex, domain, config): + """Handle errors from components: async_log_exception.""" + result.add_error( + _format_config_error(ex, domain, config), domain, config) + + # Load configuration.yaml + try: + config_path = find_config_file(config_dir) + if not config_path: + return result.add_error("File configuration.yaml not found.") + config = load_yaml_config_file(config_path) + except HomeAssistantError as err: + return result.add_error(err) + finally: + yaml.clear_secret_cache() + + # Extract and validate core [homeassistant] config + try: + core_config = config.pop(CONF_CORE, {}) + core_config = CORE_CONFIG_SCHEMA(core_config) + result[CONF_CORE] = core_config + except vol.Invalid as err: + result.add_error(err, CONF_CORE, core_config) + core_config = {} + + # Merge packages + merge_packages_config( + config, core_config.get(CONF_PACKAGES, {}), _pack_error) + del core_config[CONF_PACKAGES] + + # Filter out repeating config sections + components = set(key.split(' ')[0] for key in config.keys()) + + # Process and validate config + for domain in components: + component = get_component(domain) + if not component: + result.add_error("Component not found: {}".format(domain)) + continue + + if hasattr(component, 'CONFIG_SCHEMA'): + try: + config = component.CONFIG_SCHEMA(config) + result[domain] = config[domain] + except vol.Invalid as ex: + _comp_error(ex, domain, config) + continue + + if not hasattr(component, 'PLATFORM_SCHEMA'): + continue + + platforms = [] + for p_name, p_config in config_per_platform(config, domain): + # Validate component specific platform schema + try: + p_validated = component.PLATFORM_SCHEMA(p_config) + except vol.Invalid as ex: + _comp_error(ex, domain, config) + continue + + # Not all platform components follow same pattern for platforms + # So if p_name is None we are not going to validate platform + # (the automation component is one of them) + if p_name is None: + platforms.append(p_validated) + continue + + platform = get_platform(domain, p_name) + + if platform is None: + result.add_error( + "Platform not found: {}.{}".format(domain, p_name)) + continue + + # Validate platform specific schema + if hasattr(platform, 'PLATFORM_SCHEMA'): + # pylint: disable=no-member + try: + p_validated = platform.PLATFORM_SCHEMA(p_validated) + except vol.Invalid as ex: + _comp_error( + ex, '{}.{}'.format(domain, p_name), p_validated) + continue + + platforms.append(p_validated) + + # Remove config for current component and add validated config back in. + for filter_comp in extract_domain_configs(config, domain): + del config[filter_comp] + result[domain] = platforms + + return result diff --git a/tests/scripts/test_check_config.py b/tests/scripts/test_check_config.py index 728e683a43a..677ed8de110 100644 --- a/tests/scripts/test_check_config.py +++ b/tests/scripts/test_check_config.py @@ -1,9 +1,12 @@ """Test check_config script.""" import asyncio import logging +import os # noqa: F401 pylint: disable=unused-import import unittest +from unittest.mock import patch import homeassistant.scripts.check_config as check_config +from homeassistant.config import YAML_CONFIG_FILE from homeassistant.loader import set_component from tests.common import patch_yaml_files, get_test_config_dir @@ -21,21 +24,14 @@ BASE_CONFIG = ( ) -def change_yaml_files(check_dict): - """Change the ['yaml_files'] property and remove the configuration path. - - Also removes other files like service.yaml that gets loaded. - """ +def normalize_yaml_files(check_dict): + """Remove configuration path from ['yaml_files'].""" root = get_test_config_dir() - keys = check_dict['yaml_files'].keys() - check_dict['yaml_files'] = [] - for key in sorted(keys): - if not key.startswith('/'): - check_dict['yaml_files'].append(key) - if key.startswith(root): - check_dict['yaml_files'].append('...' + key[len(root):]) + return [key.replace(root, '...') + for key in sorted(check_dict['yaml_files'].keys())] +# pylint: disable=unsubscriptable-object class TestCheckConfig(unittest.TestCase): """Tests for the homeassistant.scripts.check_config module.""" @@ -51,176 +47,165 @@ class TestCheckConfig(unittest.TestCase): asyncio.set_event_loop(asyncio.new_event_loop()) # Will allow seeing full diff - self.maxDiff = None + self.maxDiff = None # pylint: disable=invalid-name # pylint: disable=no-self-use,invalid-name - def test_config_platform_valid(self): + @patch('os.path.isfile', return_value=True) + def test_config_platform_valid(self, isfile_patch): """Test a valid platform setup.""" files = { - 'light.yaml': BASE_CONFIG + 'light:\n platform: demo', + YAML_CONFIG_FILE: BASE_CONFIG + 'light:\n platform: demo', } with patch_yaml_files(files): - res = check_config.check(get_test_config_dir('light.yaml')) - change_yaml_files(res) - self.assertDictEqual({ - 'components': {'light': [{'platform': 'demo'}], 'group': None}, - 'except': {}, - 'secret_cache': {}, - 'secrets': {}, - 'yaml_files': ['.../light.yaml'] - }, res) + res = check_config.check(get_test_config_dir()) + assert res['components'].keys() == {'homeassistant', 'light'} + assert res['components']['light'] == [{'platform': 'demo'}] + assert res['except'] == {} + assert res['secret_cache'] == {} + assert res['secrets'] == {} + assert len(res['yaml_files']) == 1 - def test_config_component_platform_fail_validation(self): + @patch('os.path.isfile', return_value=True) + def test_config_component_platform_fail_validation(self, isfile_patch): """Test errors if component & platform not found.""" files = { - 'component.yaml': BASE_CONFIG + 'http:\n password: err123', + YAML_CONFIG_FILE: BASE_CONFIG + 'http:\n password: err123', } with patch_yaml_files(files): - res = check_config.check(get_test_config_dir('component.yaml')) - change_yaml_files(res) - - self.assertDictEqual({}, res['components']) - res['except'].pop(check_config.ERROR_STR) - self.assertDictEqual( - {'http': {'password': 'err123'}}, - res['except'] - ) - self.assertDictEqual({}, res['secret_cache']) - self.assertDictEqual({}, res['secrets']) - self.assertListEqual(['.../component.yaml'], res['yaml_files']) + res = check_config.check(get_test_config_dir()) + assert res['components'].keys() == {'homeassistant'} + assert res['except'].keys() == {'http'} + assert res['except']['http'][1] == {'http': {'password': 'err123'}} + assert res['secret_cache'] == {} + assert res['secrets'] == {} + assert len(res['yaml_files']) == 1 files = { - 'platform.yaml': (BASE_CONFIG + 'mqtt:\n\n' - 'light:\n platform: mqtt_json'), + YAML_CONFIG_FILE: (BASE_CONFIG + 'mqtt:\n\n' + 'light:\n platform: mqtt_json'), } with patch_yaml_files(files): - res = check_config.check(get_test_config_dir('platform.yaml')) - change_yaml_files(res) - self.assertDictEqual( - {'mqtt': { - 'keepalive': 60, - 'port': 1883, - 'protocol': '3.1.1', - 'discovery': False, - 'discovery_prefix': 'homeassistant', - 'tls_version': 'auto', - }, - 'light': [], - 'group': None}, - res['components'] - ) - self.assertDictEqual( - {'light.mqtt_json': {'platform': 'mqtt_json'}}, - res['except'] - ) - self.assertDictEqual({}, res['secret_cache']) - self.assertDictEqual({}, res['secrets']) - self.assertListEqual(['.../platform.yaml'], res['yaml_files']) + res = check_config.check(get_test_config_dir()) + assert res['components'].keys() == { + 'homeassistant', 'light', 'mqtt'} + assert res['components']['light'] == [] + assert res['components']['mqtt'] == { + 'keepalive': 60, + 'port': 1883, + 'protocol': '3.1.1', + 'discovery': False, + 'discovery_prefix': 'homeassistant', + 'tls_version': 'auto', + } + assert res['except'].keys() == {'light.mqtt_json'} + assert res['except']['light.mqtt_json'][1] == { + 'platform': 'mqtt_json'} + assert res['secret_cache'] == {} + assert res['secrets'] == {} + assert len(res['yaml_files']) == 1 - def test_component_platform_not_found(self): + @patch('os.path.isfile', return_value=True) + def test_component_platform_not_found(self, isfile_patch): """Test errors if component or platform not found.""" # Make sure they don't exist set_component('beer', None) - set_component('light.beer', None) files = { - 'badcomponent.yaml': BASE_CONFIG + 'beer:', - 'badplatform.yaml': BASE_CONFIG + 'light:\n platform: beer', + YAML_CONFIG_FILE: BASE_CONFIG + 'beer:', } with patch_yaml_files(files): - res = check_config.check(get_test_config_dir('badcomponent.yaml')) - change_yaml_files(res) - self.assertDictEqual({}, res['components']) - self.assertDictEqual({ - check_config.ERROR_STR: [ - 'Component not found: beer', - 'Setup failed for beer: Component not found.'] - }, res['except']) - self.assertDictEqual({}, res['secret_cache']) - self.assertDictEqual({}, res['secrets']) - self.assertListEqual(['.../badcomponent.yaml'], res['yaml_files']) + res = check_config.check(get_test_config_dir()) + assert res['components'].keys() == {'homeassistant'} + assert res['except'] == { + check_config.ERROR_STR: ['Component not found: beer']} + assert res['secret_cache'] == {} + assert res['secrets'] == {} + assert len(res['yaml_files']) == 1 - res = check_config.check(get_test_config_dir('badplatform.yaml')) - change_yaml_files(res) - assert res['components'] == {'light': [], 'group': None} + set_component('light.beer', None) + files = { + YAML_CONFIG_FILE: BASE_CONFIG + 'light:\n platform: beer', + } + with patch_yaml_files(files): + res = check_config.check(get_test_config_dir()) + assert res['components'].keys() == {'homeassistant', 'light'} + assert res['components']['light'] == [] assert res['except'] == { check_config.ERROR_STR: [ 'Platform not found: light.beer', ]} - self.assertDictEqual({}, res['secret_cache']) - self.assertDictEqual({}, res['secrets']) - self.assertListEqual(['.../badplatform.yaml'], res['yaml_files']) + assert res['secret_cache'] == {} + assert res['secrets'] == {} + assert len(res['yaml_files']) == 1 - def test_secrets(self): + @patch('os.path.isfile', return_value=True) + def test_secrets(self, isfile_patch): """Test secrets config checking method.""" + secrets_path = get_test_config_dir('secrets.yaml') + files = { - get_test_config_dir('secret.yaml'): ( - BASE_CONFIG + + get_test_config_dir(YAML_CONFIG_FILE): BASE_CONFIG + ( 'http:\n' ' api_password: !secret http_pw'), - 'secrets.yaml': ('logger: debug\n' - 'http_pw: abc123'), + secrets_path: ( + 'logger: debug\n' + 'http_pw: abc123'), } with patch_yaml_files(files): - config_path = get_test_config_dir('secret.yaml') - secrets_path = get_test_config_dir('secrets.yaml') - res = check_config.check(config_path) - change_yaml_files(res) + res = check_config.check(get_test_config_dir(), True) - # convert secrets OrderedDict to dict for assertequal - for key, val in res['secret_cache'].items(): - res['secret_cache'][key] = dict(val) + assert res['except'] == {} + assert res['components'].keys() == {'homeassistant', 'http'} + assert res['components']['http'] == { + 'api_password': 'abc123', + 'cors_allowed_origins': [], + 'ip_ban_enabled': True, + 'login_attempts_threshold': -1, + 'server_host': '0.0.0.0', + 'server_port': 8123, + 'trusted_networks': [], + 'use_x_forwarded_for': False} + assert res['secret_cache'] == {secrets_path: {'http_pw': 'abc123'}} + assert res['secrets'] == {'http_pw': 'abc123'} + assert normalize_yaml_files(res) == [ + '.../configuration.yaml', '.../secrets.yaml'] - self.assertDictEqual({ - 'components': {'http': {'api_password': 'abc123', - 'cors_allowed_origins': [], - 'ip_ban_enabled': True, - 'login_attempts_threshold': -1, - 'server_host': '0.0.0.0', - 'server_port': 8123, - 'trusted_networks': [], - 'use_x_forwarded_for': False}}, - 'except': {}, - 'secret_cache': {secrets_path: {'http_pw': 'abc123'}}, - 'secrets': {'http_pw': 'abc123'}, - 'yaml_files': ['.../secret.yaml', '.../secrets.yaml'] - }, res) - - def test_package_invalid(self): \ + @patch('os.path.isfile', return_value=True) + def test_package_invalid(self, isfile_patch): \ # pylint: disable=no-self-use,invalid-name """Test a valid platform setup.""" files = { - 'bad.yaml': BASE_CONFIG + (' packages:\n' - ' p1:\n' - ' group: ["a"]'), + YAML_CONFIG_FILE: BASE_CONFIG + ( + ' packages:\n' + ' p1:\n' + ' group: ["a"]'), } with patch_yaml_files(files): - res = check_config.check(get_test_config_dir('bad.yaml')) - change_yaml_files(res) + res = check_config.check(get_test_config_dir()) - err = res['except'].pop('homeassistant.packages.p1') - assert res['except'] == {} - assert err == {'group': ['a']} - assert res['yaml_files'] == ['.../bad.yaml'] - - assert res['components'] == {} + assert res['except'].keys() == {'homeassistant.packages.p1.group'} + assert res['except']['homeassistant.packages.p1.group'][1] == \ + {'group': ['a']} + assert len(res['except']) == 1 + assert res['components'].keys() == {'homeassistant'} + assert len(res['components']) == 1 assert res['secret_cache'] == {} assert res['secrets'] == {} + assert len(res['yaml_files']) == 1 def test_bootstrap_error(self): \ # pylint: disable=no-self-use,invalid-name """Test a valid platform setup.""" files = { - 'badbootstrap.yaml': BASE_CONFIG + 'automation: !include no.yaml', + YAML_CONFIG_FILE: BASE_CONFIG + 'automation: !include no.yaml', } with patch_yaml_files(files): - res = check_config.check(get_test_config_dir('badbootstrap.yaml')) - change_yaml_files(res) - + res = check_config.check(get_test_config_dir(YAML_CONFIG_FILE)) err = res['except'].pop(check_config.ERROR_STR) assert len(err) == 1 assert res['except'] == {} - assert res['components'] == {} + assert res['components'] == {} # No components, load failed assert res['secret_cache'] == {} assert res['secrets'] == {} + assert res['yaml_files'] == {} diff --git a/tests/test_config.py b/tests/test_config.py index 541eaf4f79e..99c21493711 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -158,11 +158,11 @@ class TestConfig(unittest.TestCase): def test_load_yaml_config_preserves_key_order(self): """Test removal of library.""" with open(YAML_PATH, 'w') as f: - f.write('hello: 0\n') + f.write('hello: 2\n') f.write('world: 1\n') self.assertEqual( - [('hello', 0), ('world', 1)], + [('hello', 2), ('world', 1)], list(config_util.load_yaml_config_file(YAML_PATH).items())) @mock.patch('homeassistant.util.location.detect_location_info', From 3ba19c52d51bb5c8cea417f19eb24251b06d30e9 Mon Sep 17 00:00:00 2001 From: mueslo Date: Fri, 9 Mar 2018 08:57:21 +0100 Subject: [PATCH 004/220] Add consider_home and source_type to device_tracker.see service (#12849) * Add consider_home and source_type to device_tracker.see service * Use schema instead of manual validation * Extend schema to validate all keys * Fix style * Set battery level to int --- .../components/device_tracker/__init__.py | 64 ++++++++++++------- homeassistant/helpers/config_validation.py | 1 + 2 files changed, 43 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index 19ab77350f3..196c11a614f 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -77,11 +77,14 @@ ATTR_MAC = 'mac' ATTR_NAME = 'name' ATTR_SOURCE_TYPE = 'source_type' ATTR_VENDOR = 'vendor' +ATTR_CONSIDER_HOME = 'consider_home' SOURCE_TYPE_GPS = 'gps' SOURCE_TYPE_ROUTER = 'router' SOURCE_TYPE_BLUETOOTH = 'bluetooth' SOURCE_TYPE_BLUETOOTH_LE = 'bluetooth_le' +SOURCE_TYPES = (SOURCE_TYPE_GPS, SOURCE_TYPE_ROUTER, + SOURCE_TYPE_BLUETOOTH, SOURCE_TYPE_BLUETOOTH_LE) NEW_DEVICE_DEFAULTS_SCHEMA = vol.Any(None, vol.Schema({ vol.Optional(CONF_TRACK_NEW, default=DEFAULT_TRACK_NEW): cv.boolean, @@ -96,6 +99,19 @@ PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend({ vol.Optional(CONF_NEW_DEVICE_DEFAULTS, default={}): NEW_DEVICE_DEFAULTS_SCHEMA }) +SERVICE_SEE_PAYLOAD_SCHEMA = vol.Schema(vol.All( + cv.has_at_least_one_key(ATTR_MAC, ATTR_DEV_ID), { + ATTR_MAC: cv.string, + ATTR_DEV_ID: cv.string, + ATTR_HOST_NAME: cv.string, + ATTR_LOCATION_NAME: cv.string, + ATTR_GPS: cv.gps, + ATTR_GPS_ACCURACY: cv.positive_int, + ATTR_BATTERY: cv.positive_int, + ATTR_ATTRIBUTES: dict, + ATTR_SOURCE_TYPE: vol.In(SOURCE_TYPES), + ATTR_CONSIDER_HOME: cv.time_period, + })) @bind_hass @@ -109,7 +125,7 @@ def is_on(hass: HomeAssistantType, entity_id: str = None): def see(hass: HomeAssistantType, mac: str = None, dev_id: str = None, host_name: str = None, location_name: str = None, gps: GPSType = None, gps_accuracy=None, - battery=None, attributes: dict = None): + battery: int = None, attributes: dict = None): """Call service to notify you see device.""" data = {key: value for key, value in ((ATTR_MAC, mac), @@ -203,12 +219,10 @@ def async_setup(hass: HomeAssistantType, config: ConfigType): @asyncio.coroutine def async_see_service(call): """Service to see a device.""" - args = {key: value for key, value in call.data.items() if key in - (ATTR_MAC, ATTR_DEV_ID, ATTR_HOST_NAME, ATTR_LOCATION_NAME, - ATTR_GPS, ATTR_GPS_ACCURACY, ATTR_BATTERY, ATTR_ATTRIBUTES)} - yield from tracker.async_see(**args) + yield from tracker.async_see(**call.data) - hass.services.async_register(DOMAIN, SERVICE_SEE, async_see_service) + hass.services.async_register( + DOMAIN, SERVICE_SEE, async_see_service, SERVICE_SEE_PAYLOAD_SCHEMA) # restore yield from tracker.async_setup_tracked_device() @@ -240,23 +254,26 @@ class DeviceTracker(object): dev.mac) def see(self, mac: str = None, dev_id: str = None, host_name: str = None, - location_name: str = None, gps: GPSType = None, gps_accuracy=None, - battery: str = None, attributes: dict = None, - source_type: str = SOURCE_TYPE_GPS, picture: str = None, - icon: str = None): + location_name: str = None, gps: GPSType = None, + gps_accuracy: int = None, battery: int = None, + attributes: dict = None, source_type: str = SOURCE_TYPE_GPS, + picture: str = None, icon: str = None, + consider_home: timedelta = None): """Notify the device tracker that you see a device.""" self.hass.add_job( self.async_see(mac, dev_id, host_name, location_name, gps, gps_accuracy, battery, attributes, source_type, - picture, icon) + picture, icon, consider_home) ) @asyncio.coroutine - def async_see(self, mac: str = None, dev_id: str = None, - host_name: str = None, location_name: str = None, - gps: GPSType = None, gps_accuracy=None, battery: str = None, - attributes: dict = None, source_type: str = SOURCE_TYPE_GPS, - picture: str = None, icon: str = None): + def async_see( + self, mac: str = None, dev_id: str = None, host_name: str = None, + location_name: str = None, gps: GPSType = None, + gps_accuracy: int = None, battery: int = None, + attributes: dict = None, source_type: str = SOURCE_TYPE_GPS, + picture: str = None, icon: str = None, + consider_home: timedelta = None): """Notify the device tracker that you see a device. This method is a coroutine. @@ -275,7 +292,7 @@ class DeviceTracker(object): if device: yield from device.async_seen( host_name, location_name, gps, gps_accuracy, battery, - attributes, source_type) + attributes, source_type, consider_home) if device.track: yield from device.async_update_ha_state() return @@ -283,7 +300,7 @@ class DeviceTracker(object): # If no device can be found, create it dev_id = util.ensure_unique_string(dev_id, self.devices.keys()) device = Device( - self.hass, self.consider_home, self.track_new, + self.hass, consider_home or self.consider_home, self.track_new, dev_id, mac, (host_name or dev_id).replace('_', ' '), picture=picture, icon=icon, hide_if_away=self.defaults.get(CONF_AWAY_HIDE, DEFAULT_AWAY_HIDE)) @@ -384,9 +401,10 @@ class Device(Entity): host_name = None # type: str location_name = None # type: str gps = None # type: GPSType - gps_accuracy = 0 + gps_accuracy = 0 # type: int last_seen = None # type: dt_util.dt.datetime - battery = None # type: str + consider_home = None # type: dt_util.dt.timedelta + battery = None # type: int attributes = None # type: dict vendor = None # type: str icon = None # type: str @@ -476,14 +494,16 @@ class Device(Entity): @asyncio.coroutine def async_seen(self, host_name: str = None, location_name: str = None, - gps: GPSType = None, gps_accuracy=0, battery: str = None, + gps: GPSType = None, gps_accuracy=0, battery: int = None, attributes: dict = None, - source_type: str = SOURCE_TYPE_GPS): + source_type: str = SOURCE_TYPE_GPS, + consider_home: timedelta = None): """Mark the device as seen.""" self.source_type = source_type self.last_seen = dt_util.utcnow() self.host_name = host_name self.location_name = location_name + self.consider_home = consider_home or self.consider_home if battery: self.battery = battery diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index f8f08fd118f..4b7c58f6e66 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -36,6 +36,7 @@ latitude = vol.All(vol.Coerce(float), vol.Range(min=-90, max=90), msg='invalid latitude') longitude = vol.All(vol.Coerce(float), vol.Range(min=-180, max=180), msg='invalid longitude') +gps = vol.ExactSequence([latitude, longitude]) sun_event = vol.All(vol.Lower, vol.Any(SUN_EVENT_SUNSET, SUN_EVENT_SUNRISE)) port = vol.All(vol.Coerce(int), vol.Range(min=1, max=65535)) From ca5f4709564773c8cebd6ee40aa7cc1095b19105 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Fri, 9 Mar 2018 15:15:39 +0100 Subject: [PATCH 005/220] Clean up Light Groups (#12962) * Clean up Light Groups * Fix tests * Remove light group from .coveragerc * async_schedule_update_ha_state called anyway --- .coveragerc | 1 - homeassistant/components/light/group.py | 39 ++++++------- tests/components/light/test_group.py | 76 ++++++++++++------------- 3 files changed, 58 insertions(+), 58 deletions(-) diff --git a/.coveragerc b/.coveragerc index 83d143f83cb..07d84523780 100644 --- a/.coveragerc +++ b/.coveragerc @@ -412,7 +412,6 @@ omit = homeassistant/components/light/decora_wifi.py homeassistant/components/light/flux_led.py homeassistant/components/light/greenwave.py - homeassistant/components/light/group.py homeassistant/components/light/hue.py homeassistant/components/light/hyperion.py homeassistant/components/light/iglo.py diff --git a/homeassistant/components/light/group.py b/homeassistant/components/light/group.py index 768754ca1af..b4a5e9dddfb 100644 --- a/homeassistant/components/light/group.py +++ b/homeassistant/components/light/group.py @@ -1,5 +1,5 @@ """ -This component allows several lights to be grouped into one light. +This platform allows several lights to be grouped into one light. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/light.group/ @@ -29,11 +29,11 @@ import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -DEFAULT_NAME = 'Group Light' +DEFAULT_NAME = 'Light Group' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Required(CONF_ENTITIES): cv.entities_domain('light') + vol.Required(CONF_ENTITIES): cv.entities_domain(light.DOMAIN) }) SUPPORT_GROUP_LIGHT = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_EFFECT @@ -44,15 +44,15 @@ SUPPORT_GROUP_LIGHT = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_EFFECT async def async_setup_platform(hass: HomeAssistantType, config: ConfigType, async_add_devices, discovery_info=None) -> None: """Initialize light.group platform.""" - async_add_devices([GroupLight(config.get(CONF_NAME), - config[CONF_ENTITIES])], True) + async_add_devices([LightGroup(config.get(CONF_NAME), + config[CONF_ENTITIES])]) -class GroupLight(light.Light): - """Representation of a group light.""" +class LightGroup(light.Light): + """Representation of a light group.""" def __init__(self, name: str, entity_ids: List[str]) -> None: - """Initialize a group light.""" + """Initialize a light group.""" self._name = name # type: str self._entity_ids = entity_ids # type: List[str] self._is_on = False # type: bool @@ -79,10 +79,11 @@ class GroupLight(light.Light): self._async_unsub_state_changed = async_track_state_change( self.hass, self._entity_ids, async_state_changed_listener) + await self.async_update() async def async_will_remove_from_hass(self): """Callback when removed from HASS.""" - if self._async_unsub_state_changed: + if self._async_unsub_state_changed is not None: self._async_unsub_state_changed() self._async_unsub_state_changed = None @@ -93,17 +94,17 @@ class GroupLight(light.Light): @property def is_on(self) -> bool: - """Return the on/off state of the light.""" + """Return the on/off state of the light group.""" return self._is_on @property def available(self) -> bool: - """Return whether the light is available.""" + """Return whether the light group is available.""" return self._available @property def brightness(self) -> Optional[int]: - """Return the brightness of this light between 0..255.""" + """Return the brightness of this light group between 0..255.""" return self._brightness @property @@ -123,17 +124,17 @@ class GroupLight(light.Light): @property def min_mireds(self) -> Optional[int]: - """Return the coldest color_temp that this light supports.""" + """Return the coldest color_temp that this light group supports.""" return self._min_mireds @property def max_mireds(self) -> Optional[int]: - """Return the warmest color_temp that this light supports.""" + """Return the warmest color_temp that this light group supports.""" return self._max_mireds @property def white_value(self) -> Optional[int]: - """Return the white value of this light between 0..255.""" + """Return the white value of this light group between 0..255.""" return self._white_value @property @@ -153,11 +154,11 @@ class GroupLight(light.Light): @property def should_poll(self) -> bool: - """No polling needed for a group light.""" + """No polling needed for a light group.""" return False async def async_turn_on(self, **kwargs): - """Forward the turn_on command to all lights in the group.""" + """Forward the turn_on command to all lights in the light group.""" data = {ATTR_ENTITY_ID: self._entity_ids} if ATTR_BRIGHTNESS in kwargs: @@ -188,7 +189,7 @@ class GroupLight(light.Light): light.DOMAIN, light.SERVICE_TURN_ON, data, blocking=True) async def async_turn_off(self, **kwargs): - """Forward the turn_off command to all lights in the group.""" + """Forward the turn_off command to all lights in the light group.""" data = {ATTR_ENTITY_ID: self._entity_ids} if ATTR_TRANSITION in kwargs: @@ -198,7 +199,7 @@ class GroupLight(light.Light): light.DOMAIN, light.SERVICE_TURN_OFF, data, blocking=True) async def async_update(self): - """Query all members and determine the group state.""" + """Query all members and determine the light group state.""" all_states = [self.hass.states.get(x) for x in self._entity_ids] states = list(filter(None, all_states)) on_states = [state for state in states if state.state == STATE_ON] diff --git a/tests/components/light/test_group.py b/tests/components/light/test_group.py index ac19f407066..3c94fa2af3e 100644 --- a/tests/components/light/test_group.py +++ b/tests/components/light/test_group.py @@ -37,22 +37,22 @@ async def test_state_reporting(hass): hass.states.async_set('light.test1', 'on') hass.states.async_set('light.test2', 'unavailable') await hass.async_block_till_done() - assert hass.states.get('light.group_light').state == 'on' + assert hass.states.get('light.light_group').state == 'on' hass.states.async_set('light.test1', 'on') hass.states.async_set('light.test2', 'off') await hass.async_block_till_done() - assert hass.states.get('light.group_light').state == 'on' + assert hass.states.get('light.light_group').state == 'on' hass.states.async_set('light.test1', 'off') hass.states.async_set('light.test2', 'off') await hass.async_block_till_done() - assert hass.states.get('light.group_light').state == 'off' + assert hass.states.get('light.light_group').state == 'off' hass.states.async_set('light.test1', 'unavailable') hass.states.async_set('light.test2', 'unavailable') await hass.async_block_till_done() - assert hass.states.get('light.group_light').state == 'unavailable' + assert hass.states.get('light.light_group').state == 'unavailable' async def test_brightness(hass): @@ -64,7 +64,7 @@ async def test_brightness(hass): hass.states.async_set('light.test1', 'on', {'brightness': 255, 'supported_features': 1}) await hass.async_block_till_done() - state = hass.states.get('light.group_light') + state = hass.states.get('light.light_group') assert state.state == 'on' assert state.attributes['supported_features'] == 1 assert state.attributes['brightness'] == 255 @@ -72,14 +72,14 @@ async def test_brightness(hass): hass.states.async_set('light.test2', 'on', {'brightness': 100, 'supported_features': 1}) await hass.async_block_till_done() - state = hass.states.get('light.group_light') + state = hass.states.get('light.light_group') assert state.state == 'on' assert state.attributes['brightness'] == 177 hass.states.async_set('light.test1', 'off', {'brightness': 255, 'supported_features': 1}) await hass.async_block_till_done() - state = hass.states.get('light.group_light') + state = hass.states.get('light.light_group') assert state.state == 'on' assert state.attributes['supported_features'] == 1 assert state.attributes['brightness'] == 100 @@ -94,7 +94,7 @@ async def test_xy_color(hass): hass.states.async_set('light.test1', 'on', {'xy_color': (1.0, 1.0), 'supported_features': 64}) await hass.async_block_till_done() - state = hass.states.get('light.group_light') + state = hass.states.get('light.light_group') assert state.state == 'on' assert state.attributes['supported_features'] == 64 assert state.attributes['xy_color'] == (1.0, 1.0) @@ -102,14 +102,14 @@ async def test_xy_color(hass): hass.states.async_set('light.test2', 'on', {'xy_color': (0.5, 0.5), 'supported_features': 64}) await hass.async_block_till_done() - state = hass.states.get('light.group_light') + state = hass.states.get('light.light_group') assert state.state == 'on' assert state.attributes['xy_color'] == (0.75, 0.75) hass.states.async_set('light.test1', 'off', {'xy_color': (1.0, 1.0), 'supported_features': 64}) await hass.async_block_till_done() - state = hass.states.get('light.group_light') + state = hass.states.get('light.light_group') assert state.state == 'on' assert state.attributes['xy_color'] == (0.5, 0.5) @@ -123,7 +123,7 @@ async def test_rgb_color(hass): hass.states.async_set('light.test1', 'on', {'rgb_color': (255, 0, 0), 'supported_features': 16}) await hass.async_block_till_done() - state = hass.states.get('light.group_light') + state = hass.states.get('light.light_group') assert state.state == 'on' assert state.attributes['supported_features'] == 16 assert state.attributes['rgb_color'] == (255, 0, 0) @@ -132,13 +132,13 @@ async def test_rgb_color(hass): {'rgb_color': (255, 255, 255), 'supported_features': 16}) await hass.async_block_till_done() - state = hass.states.get('light.group_light') + state = hass.states.get('light.light_group') assert state.attributes['rgb_color'] == (255, 127, 127) hass.states.async_set('light.test1', 'off', {'rgb_color': (255, 0, 0), 'supported_features': 16}) await hass.async_block_till_done() - state = hass.states.get('light.group_light') + state = hass.states.get('light.light_group') assert state.attributes['rgb_color'] == (255, 255, 255) @@ -151,19 +151,19 @@ async def test_white_value(hass): hass.states.async_set('light.test1', 'on', {'white_value': 255, 'supported_features': 128}) await hass.async_block_till_done() - state = hass.states.get('light.group_light') + state = hass.states.get('light.light_group') assert state.attributes['white_value'] == 255 hass.states.async_set('light.test2', 'on', {'white_value': 100, 'supported_features': 128}) await hass.async_block_till_done() - state = hass.states.get('light.group_light') + state = hass.states.get('light.light_group') assert state.attributes['white_value'] == 177 hass.states.async_set('light.test1', 'off', {'white_value': 255, 'supported_features': 128}) await hass.async_block_till_done() - state = hass.states.get('light.group_light') + state = hass.states.get('light.light_group') assert state.attributes['white_value'] == 100 @@ -176,19 +176,19 @@ async def test_color_temp(hass): hass.states.async_set('light.test1', 'on', {'color_temp': 2, 'supported_features': 2}) await hass.async_block_till_done() - state = hass.states.get('light.group_light') + state = hass.states.get('light.light_group') assert state.attributes['color_temp'] == 2 hass.states.async_set('light.test2', 'on', {'color_temp': 1000, 'supported_features': 2}) await hass.async_block_till_done() - state = hass.states.get('light.group_light') + state = hass.states.get('light.light_group') assert state.attributes['color_temp'] == 501 hass.states.async_set('light.test1', 'off', {'color_temp': 2, 'supported_features': 2}) await hass.async_block_till_done() - state = hass.states.get('light.group_light') + state = hass.states.get('light.light_group') assert state.attributes['color_temp'] == 1000 @@ -202,7 +202,7 @@ async def test_min_max_mireds(hass): {'min_mireds': 2, 'max_mireds': 5, 'supported_features': 2}) await hass.async_block_till_done() - state = hass.states.get('light.group_light') + state = hass.states.get('light.light_group') assert state.attributes['min_mireds'] == 2 assert state.attributes['max_mireds'] == 5 @@ -210,7 +210,7 @@ async def test_min_max_mireds(hass): {'min_mireds': 7, 'max_mireds': 1234567890, 'supported_features': 2}) await hass.async_block_till_done() - state = hass.states.get('light.group_light') + state = hass.states.get('light.light_group') assert state.attributes['min_mireds'] == 2 assert state.attributes['max_mireds'] == 1234567890 @@ -218,7 +218,7 @@ async def test_min_max_mireds(hass): {'min_mireds': 1, 'max_mireds': 2, 'supported_features': 2}) await hass.async_block_till_done() - state = hass.states.get('light.group_light') + state = hass.states.get('light.light_group') assert state.attributes['min_mireds'] == 1 assert state.attributes['max_mireds'] == 1234567890 @@ -232,21 +232,21 @@ async def test_effect_list(hass): hass.states.async_set('light.test1', 'on', {'effect_list': ['None', 'Random', 'Colorloop']}) await hass.async_block_till_done() - state = hass.states.get('light.group_light') + state = hass.states.get('light.light_group') assert set(state.attributes['effect_list']) == { 'None', 'Random', 'Colorloop'} hass.states.async_set('light.test2', 'on', {'effect_list': ['None', 'Random', 'Rainbow']}) await hass.async_block_till_done() - state = hass.states.get('light.group_light') + state = hass.states.get('light.light_group') assert set(state.attributes['effect_list']) == { 'None', 'Random', 'Colorloop', 'Rainbow'} hass.states.async_set('light.test1', 'off', {'effect_list': ['None', 'Colorloop', 'Seven']}) await hass.async_block_till_done() - state = hass.states.get('light.group_light') + state = hass.states.get('light.light_group') assert set(state.attributes['effect_list']) == { 'None', 'Random', 'Colorloop', 'Seven', 'Rainbow'} @@ -261,19 +261,19 @@ async def test_effect(hass): hass.states.async_set('light.test1', 'on', {'effect': 'None', 'supported_features': 2}) await hass.async_block_till_done() - state = hass.states.get('light.group_light') + state = hass.states.get('light.light_group') assert state.attributes['effect'] == 'None' hass.states.async_set('light.test2', 'on', {'effect': 'None', 'supported_features': 2}) await hass.async_block_till_done() - state = hass.states.get('light.group_light') + state = hass.states.get('light.light_group') assert state.attributes['effect'] == 'None' hass.states.async_set('light.test3', 'on', {'effect': 'Random', 'supported_features': 2}) await hass.async_block_till_done() - state = hass.states.get('light.group_light') + state = hass.states.get('light.light_group') assert state.attributes['effect'] == 'None' hass.states.async_set('light.test1', 'off', @@ -281,7 +281,7 @@ async def test_effect(hass): hass.states.async_set('light.test2', 'off', {'effect': 'None', 'supported_features': 2}) await hass.async_block_till_done() - state = hass.states.get('light.group_light') + state = hass.states.get('light.light_group') assert state.attributes['effect'] == 'Random' @@ -294,25 +294,25 @@ async def test_supported_features(hass): hass.states.async_set('light.test1', 'on', {'supported_features': 0}) await hass.async_block_till_done() - state = hass.states.get('light.group_light') + state = hass.states.get('light.light_group') assert state.attributes['supported_features'] == 0 hass.states.async_set('light.test2', 'on', {'supported_features': 2}) await hass.async_block_till_done() - state = hass.states.get('light.group_light') + state = hass.states.get('light.light_group') assert state.attributes['supported_features'] == 2 hass.states.async_set('light.test1', 'off', {'supported_features': 41}) await hass.async_block_till_done() - state = hass.states.get('light.group_light') + state = hass.states.get('light.light_group') assert state.attributes['supported_features'] == 43 hass.states.async_set('light.test2', 'off', {'supported_features': 256}) await hass.async_block_till_done() - state = hass.states.get('light.group_light') + state = hass.states.get('light.light_group') assert state.attributes['supported_features'] == 41 @@ -326,29 +326,29 @@ async def test_service_calls(hass): ]}) await hass.async_block_till_done() - assert hass.states.get('light.group_light').state == 'on' - light.async_toggle(hass, 'light.group_light') + assert hass.states.get('light.light_group').state == 'on' + light.async_toggle(hass, 'light.light_group') await hass.async_block_till_done() assert hass.states.get('light.bed_light').state == 'off' assert hass.states.get('light.ceiling_lights').state == 'off' assert hass.states.get('light.kitchen_lights').state == 'off' - light.async_turn_on(hass, 'light.group_light') + light.async_turn_on(hass, 'light.light_group') await hass.async_block_till_done() assert hass.states.get('light.bed_light').state == 'on' assert hass.states.get('light.ceiling_lights').state == 'on' assert hass.states.get('light.kitchen_lights').state == 'on' - light.async_turn_off(hass, 'light.group_light') + light.async_turn_off(hass, 'light.light_group') await hass.async_block_till_done() assert hass.states.get('light.bed_light').state == 'off' assert hass.states.get('light.ceiling_lights').state == 'off' assert hass.states.get('light.kitchen_lights').state == 'off' - light.async_turn_on(hass, 'light.group_light', brightness=128, + light.async_turn_on(hass, 'light.light_group', brightness=128, effect='Random', rgb_color=(42, 255, 255)) await hass.async_block_till_done() From ecaf0189cca74e15a12f1b5ffe496f76d3a814d2 Mon Sep 17 00:00:00 2001 From: Ryan McLean Date: Fri, 9 Mar 2018 16:50:21 +0000 Subject: [PATCH 006/220] Plex mark devices unavailable if they 'vanish' and clear media (#12811) * Marks Devices unavailable if they 'vanish' and clears media * Fixed PEP8 complaint * Fixed Linting * Lint Fix * Fix redine of id * More lint fixes * Removed redundant loop for setting availability of client Renamed '_is_device_available' to '_available' Renamed 'available_ids' to 'available_client_ids' * removed whitespace per houndCI --- homeassistant/components/media_player/plex.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/homeassistant/components/media_player/plex.py b/homeassistant/components/media_player/plex.py index a63bf8525ed..caa81424377 100644 --- a/homeassistant/components/media_player/plex.py +++ b/homeassistant/components/media_player/plex.py @@ -154,11 +154,14 @@ def setup_plexserver( return new_plex_clients = [] + available_client_ids = [] for device in devices: # For now, let's allow all deviceClass types if device.deviceClass in ['badClient']: continue + available_client_ids.append(device.machineIdentifier) + if device.machineIdentifier not in plex_clients: new_client = PlexClient(config, device, None, plex_sessions, update_devices, @@ -186,6 +189,9 @@ def setup_plexserver( if client.session is None: client.force_idle() + client.set_availability(client.machine_identifier + in available_client_ids) + if new_plex_clients: add_devices_callback(new_plex_clients) @@ -259,6 +265,7 @@ class PlexClient(MediaPlayerDevice): """Initialize the Plex device.""" self._app_name = '' self._device = None + self._available = False self._device_protocol_capabilities = None self._is_player_active = False self._is_player_available = False @@ -407,6 +414,12 @@ class PlexClient(MediaPlayerDevice): self._media_image_url = thumb_url + def set_availability(self, available): + """Set the device as available/unavailable noting time.""" + if not available: + self._clear_media_details() + self._available = available + def _set_player_state(self): if self._player_state == 'playing': self._is_player_active = True @@ -468,6 +481,11 @@ class PlexClient(MediaPlayerDevice): """Return the id of this plex client.""" return self.machine_identifier + @property + def available(self): + """Return the availability of the client.""" + return self._available + @property def name(self): """Return the name of the device.""" From d8a7c547dfeae3dcccd01285bedcedbb668b2ffc Mon Sep 17 00:00:00 2001 From: Ryan McLean Date: Fri, 9 Mar 2018 16:50:39 +0000 Subject: [PATCH 007/220] Updated to plexapi 3.0.6 (#13005) --- homeassistant/components/media_player/plex.py | 2 +- homeassistant/components/sensor/plex.py | 2 +- requirements_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/media_player/plex.py b/homeassistant/components/media_player/plex.py index caa81424377..48e532074f7 100644 --- a/homeassistant/components/media_player/plex.py +++ b/homeassistant/components/media_player/plex.py @@ -24,7 +24,7 @@ from homeassistant.helpers import config_validation as cv from homeassistant.helpers.event import track_utc_time_change from homeassistant.util.json import load_json, save_json -REQUIREMENTS = ['plexapi==3.0.5'] +REQUIREMENTS = ['plexapi==3.0.6'] _CONFIGURING = {} _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sensor/plex.py b/homeassistant/components/sensor/plex.py index b0c40e8f007..87af51d2bbd 100644 --- a/homeassistant/components/sensor/plex.py +++ b/homeassistant/components/sensor/plex.py @@ -16,7 +16,7 @@ from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['plexapi==3.0.5'] +REQUIREMENTS = ['plexapi==3.0.6'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index b7af19f0d66..c233e528403 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -588,7 +588,7 @@ pizzapi==0.0.3 # homeassistant.components.media_player.plex # homeassistant.components.sensor.plex -plexapi==3.0.5 +plexapi==3.0.6 # homeassistant.components.sensor.mhz19 # homeassistant.components.sensor.serial_pm From 37d8cd7b7527ee06adad99736f3c4f5646373dac Mon Sep 17 00:00:00 2001 From: Johann Kellerman Date: Fri, 9 Mar 2018 22:27:39 +0200 Subject: [PATCH 008/220] New lazytox.py script (#12862) --- .github/PULL_REQUEST_TEMPLATE.md | 3 +- script/gen_requirements_all.py | 16 ++- script/lazytox.py | 235 +++++++++++++++++++++++++++++++ script/lint | 38 +++-- 4 files changed, 262 insertions(+), 30 deletions(-) create mode 100644 script/lazytox.py diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 43e1c399671..9a8e6812cf3 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -12,19 +12,18 @@ ## Checklist: - [ ] The code change is tested and works locally. + - [ ] Local tests pass with `tox`. **Your PR cannot be merged unless tests pass** If user exposed functionality or configuration variables are added/changed: - [ ] Documentation added/updated in [home-assistant.github.io](https://github.com/home-assistant/home-assistant.github.io) If the code communicates with devices, web services, or third-party tools: - - [ ] Local tests with `tox` run successfully. **Your PR cannot be merged unless tests pass** - [ ] New dependencies have been added to the `REQUIREMENTS` variable ([example][ex-requir]). - [ ] New dependencies are only imported inside functions that use them ([example][ex-import]). - [ ] New dependencies have been added to `requirements_all.txt` by running `script/gen_requirements_all.py`. - [ ] New files were added to `.coveragerc`. If the code does not interact with devices: - - [ ] Local tests with `tox` run successfully. **Your PR cannot be merged unless tests pass** - [ ] Tests have been added to verify that the new code works. [ex-requir]: https://github.com/home-assistant/home-assistant/blob/dev/homeassistant/components/keyboard.py#L14 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index a9a68d09491..a7704088e26 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -277,23 +277,23 @@ def validate_constraints_file(data): return data + CONSTRAINT_BASE == req_file.read() -def main(): +def main(validate): """Main section of the script.""" if not os.path.isfile('requirements_all.txt'): print('Run this from HA root dir') - return + return 1 data = gather_modules() if data is None: - sys.exit(1) + return 1 constraints = gather_constraints() reqs_file = requirements_all_output(data) reqs_test_file = requirements_test_output(data) - if sys.argv[-1] == 'validate': + if validate: errors = [] if not validate_requirements_file(reqs_file): errors.append("requirements_all.txt is not up to date") @@ -309,14 +309,16 @@ def main(): print("******* ERROR") print('\n'.join(errors)) print("Please run script/gen_requirements_all.py") - sys.exit(1) + return 1 - sys.exit(0) + return 0 write_requirements_file(reqs_file) write_test_requirements_file(reqs_test_file) write_constraints_file(constraints) + return 0 if __name__ == '__main__': - main() + _VAL = sys.argv[-1] == 'validate' + sys.exit(main(_VAL)) diff --git a/script/lazytox.py b/script/lazytox.py new file mode 100644 index 00000000000..2137ae1794c --- /dev/null +++ b/script/lazytox.py @@ -0,0 +1,235 @@ +#!/usr/bin/env python3 +""" +Lazy 'tox' to quickly check if branch is up to PR standards. + +This is NOT a tox replacement, only a quick check during development. +""" +import os +import asyncio +import sys +import re +import shlex +from collections import namedtuple + +try: + from colorlog.escape_codes import escape_codes +except ImportError: + escape_codes = None + + +RE_ASCII = re.compile(r"\033\[[^m]*m") +Error = namedtuple('Error', ['file', 'line', 'col', 'msg']) + +PASS = 'green' +FAIL = 'bold_red' + + +def printc(the_color, *args): + """Color print helper.""" + msg = ' '.join(args) + if not escape_codes: + print(msg) + return + try: + print(escape_codes[the_color] + msg + escape_codes['reset']) + except KeyError: + print(msg) + raise ValueError("Invalid color {}".format(the_color)) + + +def validate_requirements_ok(): + """Validate requirements, returns True of ok.""" + # pylint: disable=E0402 + from gen_requirements_all import main as req_main + return req_main(True) == 0 + + +async def read_stream(stream, display): + """Read from stream line by line until EOF, display, and capture lines.""" + output = [] + while True: + line = await stream.readline() + if not line: + break + output.append(line) + display(line.decode()) # assume it doesn't block + return b''.join(output) + + +async def async_exec(*args, display=False): + """Execute, return code & log.""" + argsp = [] + for arg in args: + if os.path.isfile(arg): + argsp.append("\\\n {}".format(shlex.quote(arg))) + else: + argsp.append(shlex.quote(arg)) + printc('cyan', *argsp) + try: + kwargs = {'loop': LOOP, 'stdout': asyncio.subprocess.PIPE, + 'stderr': asyncio.subprocess.STDOUT} + if display: + kwargs['stderr'] = asyncio.subprocess.PIPE + # pylint: disable=E1120 + proc = await asyncio.create_subprocess_exec(*args, **kwargs) + except FileNotFoundError as err: + printc(FAIL, "Could not execute {}. Did you install test requirements?" + .format(args[0])) + raise err + + if not display: + # Readin stdout into log + stdout, _ = await proc.communicate() + else: + # read child's stdout/stderr concurrently (capture and display) + stdout, _ = await asyncio.gather( + read_stream(proc.stdout, sys.stdout.write), + read_stream(proc.stderr, sys.stderr.write)) + exit_code = await proc.wait() + stdout = stdout.decode('utf-8') + return exit_code, stdout + + +async def git(): + """Exec git.""" + if len(sys.argv) > 2 and sys.argv[1] == '--': + return sys.argv[2:] + _, log = await async_exec('git', 'diff', 'upstream/dev...', '--name-only') + return log.splitlines() + + +async def pylint(files): + """Exec pylint.""" + _, log = await async_exec('pylint', '-f', 'parseable', '--persistent=n', + *files) + res = [] + for line in log.splitlines(): + line = line.split(':') + if len(line) < 3: + continue + res.append(Error(line[0].replace('\\', '/'), + line[1], "", line[2].strip())) + return res + + +async def flake8(files): + """Exec flake8.""" + _, log = await async_exec('flake8', '--doctests', *files) + res = [] + for line in log.splitlines(): + line = line.split(':') + if len(line) < 4: + continue + res.append(Error(line[0].replace('\\', '/'), + line[1], line[2], line[3].strip())) + return res + + +async def lint(files): + """Perform lint.""" + fres, pres = await asyncio.gather(flake8(files), pylint(files)) + + res = fres + pres + res.sort(key=lambda item: item.file) + if res: + print("Pylint & Flake8 errors:") + else: + printc(PASS, "Pylint and Flake8 passed") + + lint_ok = True + for err in res: + err_msg = "{} {}:{} {}".format(err.file, err.line, err.col, err.msg) + + # tests/* does not have to pass lint + if err.file.startswith('tests/'): + print(err_msg) + else: + printc(FAIL, err_msg) + lint_ok = False + + return lint_ok + + +async def main(): + """The main loop.""" + # Ensure we are in the homeassistant root + os.chdir(os.path.dirname(os.path.dirname(os.path.realpath(__file__)))) + + files = await git() + if not files: + print("No changed files found. Please ensure you have added your " + "changes with git add & git commit") + return + + pyfile = re.compile(r".+\.py$") + pyfiles = [file for file in files if pyfile.match(file)] + + print("=============================") + printc('bold', "CHANGED FILES:\n", '\n '.join(pyfiles)) + print("=============================") + + skip_lint = len(sys.argv) > 1 and sys.argv[1] == '--skiplint' + if skip_lint: + printc(FAIL, "LINT DISABLED") + elif not await lint(pyfiles): + printc(FAIL, "Please fix your lint issues before continuing") + return + + test_files = set() + gen_req = False + for fname in pyfiles: + if fname.startswith('homeassistant/components/'): + gen_req = True # requirements script for components + # Find test files... + if fname.startswith('tests/'): + if '/test_' in fname: # All test helpers should be excluded + test_files.add(fname) + else: + parts = fname.split('/') + parts[0] = 'tests' + if parts[-1] == '__init__.py': + parts[-1] = 'test_init.py' + elif parts[-1] == '__main__.py': + parts[-1] = 'test_main.py' + else: + parts[-1] = 'test_' + parts[-1] + fname = '/'.join(parts) + if os.path.isfile(fname): + test_files.add(fname) + + if gen_req: + print("=============================") + if validate_requirements_ok(): + printc(PASS, "script/gen_requirements.py passed") + else: + printc(FAIL, "Please run script/gen_requirements.py") + return + + print("=============================") + if not test_files: + print("No test files identified, ideally you should run tox") + return + + code, _ = await async_exec( + 'pytest', '-vv', '--force-sugar', '--', *test_files, display=True) + print("=============================") + + if code == 0: + printc(PASS, "Yay! This will most likely pass tox") + else: + printc(FAIL, "Tests not passing") + + if skip_lint: + printc(FAIL, "LINT DISABLED") + + +if __name__ == '__main__': + LOOP = asyncio.ProactorEventLoop() if sys.platform == 'win32' \ + else asyncio.get_event_loop() + + try: + LOOP.run_until_complete(main()) + except (FileNotFoundError, KeyboardInterrupt): + pass + finally: + LOOP.close() diff --git a/script/lint b/script/lint index bfce996788e..9d994429f74 100755 --- a/script/lint +++ b/script/lint @@ -3,25 +3,21 @@ cd "$(dirname "$0")/.." -if [ "$1" = "--all" ]; then - tox -e lint -else - export files="`git diff upstream/dev... --name-only | grep -e '\.py$'`" - echo "=================================================" - echo "FILES CHANGED (git diff upstream/dev... --name-only)" - echo "=================================================" - if [ -z "$files" ] ; then - echo "No python file changed" - exit - fi - printf "%s\n" $files - echo "================" - echo "LINT with flake8" - echo "================" - flake8 --doctests $files - echo "================" - echo "LINT with pylint" - echo "================" - pylint $files - echo +export files="`git diff upstream/dev... --name-only | grep -e '\.py$'`" +echo "=================================================" +echo "FILES CHANGED (git diff upstream/dev... --name-only)" +echo "=================================================" +if [ -z "$files" ] ; then + echo "No python file changed. Rather use: tox -e lint" + exit fi +printf "%s\n" $files +echo "================" +echo "LINT with flake8" +echo "================" +flake8 --doctests $files +echo "================" +echo "LINT with pylint" +echo "================" +pylint $files +echo From 11e1b8a19d9050196aa239594cbe05c79e9165f7 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 9 Mar 2018 13:04:23 -0800 Subject: [PATCH 009/220] Update netdisco to 1.3.0 (#13007) --- 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 d1045143bb2..21a339602dd 100644 --- a/homeassistant/components/discovery.py +++ b/homeassistant/components/discovery.py @@ -21,7 +21,7 @@ from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.helpers.discovery import async_load_platform, async_discover import homeassistant.util.dt as dt_util -REQUIREMENTS = ['netdisco==1.2.4'] +REQUIREMENTS = ['netdisco==1.3.0'] DOMAIN = 'discovery' diff --git a/requirements_all.txt b/requirements_all.txt index c233e528403..cdf627b8507 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -512,7 +512,7 @@ myusps==1.3.2 nad_receiver==0.0.9 # homeassistant.components.discovery -netdisco==1.2.4 +netdisco==1.3.0 # homeassistant.components.sensor.neurio_energy neurio==0.3.1 From 05255b9c3fbe3c482c5d5d5d56e147f1408e3d8e Mon Sep 17 00:00:00 2001 From: Johann Kellerman Date: Sat, 10 Mar 2018 01:52:21 +0200 Subject: [PATCH 010/220] Safe fix for #13015 (#13024) --- homeassistant/bootstrap.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 2f093f061d9..50d8502bbd1 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -113,7 +113,10 @@ def async_from_config_dict(config: Dict[str, Any], yield from hass.async_add_job(loader.prepare, hass) # Make a copy because we are mutating it. - config = OrderedDict(config) + new_config = OrderedDict() + for key, value in config.items(): + new_config[key] = value or {} + config = new_config # Merge packages conf_util.merge_packages_config( From 556901ea48c560f9ddfbd20cffb9de387f718965 Mon Sep 17 00:00:00 2001 From: Heiko Thiery Date: Sat, 10 Mar 2018 00:53:28 +0100 Subject: [PATCH 011/220] remove rounding of temperature reading (#13018) With homeassistant 0.65.0 the filter sensor is introduced. Now there is a common way to filter the peaks comming from the readings. Signed-off-by: Heiko Thiery --- homeassistant/components/sensor/lacrosse.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/lacrosse.py b/homeassistant/components/sensor/lacrosse.py index 3e0a5af283f..034f0be49f6 100644 --- a/homeassistant/components/sensor/lacrosse.py +++ b/homeassistant/components/sensor/lacrosse.py @@ -157,7 +157,7 @@ class LaCrosseSensor(Entity): self._expiration_trigger = async_track_point_in_utc_time( self.hass, self.value_is_expired, expiration_at) - self._temperature = round(lacrosse_sensor.temperature * 2) / 2 + self._temperature = lacrosse_sensor.temperature self._humidity = lacrosse_sensor.humidity self._low_battery = lacrosse_sensor.low_battery self._new_battery = lacrosse_sensor.new_battery From 652e0d45a90641cb5566bb422e00f1f8fea0d613 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 9 Mar 2018 19:38:33 -0800 Subject: [PATCH 012/220] Add support for input boolean to Google Assistant (#13030) --- .../components/google_assistant/trait.py | 2 + .../components/google_assistant/test_trait.py | 40 ++++++++++++++++++- 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index dd7b761e782..c78d70e21e6 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -5,6 +5,7 @@ from homeassistant.components import ( cover, group, fan, + input_boolean, media_player, light, scene, @@ -182,6 +183,7 @@ class OnOffTrait(_Trait): """Test if state is supported.""" return domain in ( group.DOMAIN, + input_boolean.DOMAIN, switch.DOMAIN, fan.DOMAIN, light.DOMAIN, diff --git a/tests/components/google_assistant/test_trait.py b/tests/components/google_assistant/test_trait.py index 90dd5d33581..4ffb273662e 100644 --- a/tests/components/google_assistant/test_trait.py +++ b/tests/components/google_assistant/test_trait.py @@ -9,8 +9,9 @@ from homeassistant.components import ( climate, cover, fan, - media_player, + input_boolean, light, + media_player, scene, script, switch, @@ -138,6 +139,43 @@ async def test_onoff_group(hass): } +async def test_onoff_input_boolean(hass): + """Test OnOff trait support for input_boolean domain.""" + assert trait.OnOffTrait.supported(media_player.DOMAIN, 0) + + trt_on = trait.OnOffTrait(State('input_boolean.bla', STATE_ON)) + + assert trt_on.sync_attributes() == {} + + assert trt_on.query_attributes() == { + 'on': True + } + + trt_off = trait.OnOffTrait(State('input_boolean.bla', STATE_OFF)) + assert trt_off.query_attributes() == { + 'on': False + } + + on_calls = async_mock_service(hass, input_boolean.DOMAIN, SERVICE_TURN_ON) + await trt_on.execute(hass, trait.COMMAND_ONOFF, { + 'on': True + }) + assert len(on_calls) == 1 + assert on_calls[0].data == { + ATTR_ENTITY_ID: 'input_boolean.bla', + } + + off_calls = async_mock_service(hass, input_boolean.DOMAIN, + SERVICE_TURN_OFF) + await trt_on.execute(hass, trait.COMMAND_ONOFF, { + 'on': False + }) + assert len(off_calls) == 1 + assert off_calls[0].data == { + ATTR_ENTITY_ID: 'input_boolean.bla', + } + + async def test_onoff_switch(hass): """Test OnOff trait support for switch domain.""" assert trait.OnOffTrait.supported(media_player.DOMAIN, 0) From 36361d623db91e1354c03ce4149df403c91402d2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 9 Mar 2018 19:38:51 -0800 Subject: [PATCH 013/220] Make Throttle async aware (#13027) * Make Throttle async aware * Lint --- .../components/media_player/bluesound.py | 22 ++++++++----------- .../components/media_player/volumio.py | 5 ++--- homeassistant/components/sensor/fido.py | 5 ++--- .../components/sensor/hydroquebec.py | 5 ++--- homeassistant/components/sensor/luftdaten.py | 13 ++++------- homeassistant/components/sensor/sabnzbd.py | 5 ++--- homeassistant/components/sensor/startca.py | 7 +++--- homeassistant/components/sensor/teksavvy.py | 7 +++--- .../components/sensor/wunderground.py | 7 +++--- homeassistant/util/__init__.py | 15 +++++++++++-- tests/util/test_init.py | 11 ++++++++++ 11 files changed, 54 insertions(+), 48 deletions(-) diff --git a/homeassistant/components/media_player/bluesound.py b/homeassistant/components/media_player/bluesound.py index d308b94e64c..a07e577c969 100644 --- a/homeassistant/components/media_player/bluesound.py +++ b/homeassistant/components/media_player/bluesound.py @@ -423,19 +423,17 @@ class BluesoundPlayer(MediaPlayerDevice): for player in self._hass.data[DATA_BLUESOUND]: yield from player.force_update_sync_status() - @asyncio.coroutine @Throttle(SYNC_STATUS_INTERVAL) - def async_update_sync_status(self, on_updated_cb=None, - raise_timeout=False): + async def async_update_sync_status(self, on_updated_cb=None, + raise_timeout=False): """Update sync status.""" - yield from self.force_update_sync_status( + await self.force_update_sync_status( on_updated_cb, raise_timeout=False) - @asyncio.coroutine @Throttle(UPDATE_CAPTURE_INTERVAL) - def async_update_captures(self): + async def async_update_captures(self): """Update Capture sources.""" - resp = yield from self.send_bluesound_command( + resp = await self.send_bluesound_command( 'RadioBrowse?service=Capture') if not resp: return @@ -459,11 +457,10 @@ class BluesoundPlayer(MediaPlayerDevice): return self._capture_items - @asyncio.coroutine @Throttle(UPDATE_PRESETS_INTERVAL) - def async_update_presets(self): + async def async_update_presets(self): """Update Presets.""" - resp = yield from self.send_bluesound_command('Presets') + resp = await self.send_bluesound_command('Presets') if not resp: return self._preset_items = [] @@ -488,11 +485,10 @@ class BluesoundPlayer(MediaPlayerDevice): return self._preset_items - @asyncio.coroutine @Throttle(UPDATE_SERVICES_INTERVAL) - def async_update_services(self): + async def async_update_services(self): """Update Services.""" - resp = yield from self.send_bluesound_command('Services') + resp = await self.send_bluesound_command('Services') if not resp: return self._services_items = [] diff --git a/homeassistant/components/media_player/volumio.py b/homeassistant/components/media_player/volumio.py index 84b957533fe..0a940c0aa9d 100644 --- a/homeassistant/components/media_player/volumio.py +++ b/homeassistant/components/media_player/volumio.py @@ -253,8 +253,7 @@ class Volumio(MediaPlayerDevice): return self.send_volumio_msg('commands', params={'cmd': 'clearQueue'}) - @asyncio.coroutine @Throttle(PLAYLIST_UPDATE_INTERVAL) - def _async_update_playlists(self, **kwargs): + async def _async_update_playlists(self, **kwargs): """Update available Volumio playlists.""" - self._playlists = yield from self.send_volumio_msg('listplaylists') + self._playlists = await self.send_volumio_msg('listplaylists') diff --git a/homeassistant/components/sensor/fido.py b/homeassistant/components/sensor/fido.py index 4fc79745b99..25a104bf259 100644 --- a/homeassistant/components/sensor/fido.py +++ b/homeassistant/components/sensor/fido.py @@ -157,13 +157,12 @@ class FidoData(object): REQUESTS_TIMEOUT, httpsession) self.data = {} - @asyncio.coroutine @Throttle(MIN_TIME_BETWEEN_UPDATES) - def async_update(self): + async def async_update(self): """Get the latest data from Fido.""" from pyfido.client import PyFidoError try: - yield from self.client.fetch_data() + await self.client.fetch_data() except PyFidoError as exp: _LOGGER.error("Error on receive last Fido data: %s", exp) return False diff --git a/homeassistant/components/sensor/hydroquebec.py b/homeassistant/components/sensor/hydroquebec.py index e10abc14ff1..3678ac9268f 100644 --- a/homeassistant/components/sensor/hydroquebec.py +++ b/homeassistant/components/sensor/hydroquebec.py @@ -182,13 +182,12 @@ class HydroquebecData(object): return self.client.get_contracts() return [] - @asyncio.coroutine @Throttle(MIN_TIME_BETWEEN_UPDATES) - def _fetch_data(self): + async def _fetch_data(self): """Fetch latest data from HydroQuebec.""" from pyhydroquebec.client import PyHydroQuebecError try: - yield from self.client.fetch_data() + await self.client.fetch_data() except PyHydroQuebecError as exp: _LOGGER.error("Error on receive last Hydroquebec data: %s", exp) return False diff --git a/homeassistant/components/sensor/luftdaten.py b/homeassistant/components/sensor/luftdaten.py index 72ee8a7ce93..c5e0b12b0e0 100644 --- a/homeassistant/components/sensor/luftdaten.py +++ b/homeassistant/components/sensor/luftdaten.py @@ -133,13 +133,9 @@ class LuftdatenSensor(Entity): except KeyError: return - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Get the latest data from luftdaten.info and update the state.""" - try: - yield from self.luftdaten.async_update() - except TypeError: - pass + await self.luftdaten.async_update() class LuftdatenData(object): @@ -150,12 +146,11 @@ class LuftdatenData(object): self.data = data @Throttle(MIN_TIME_BETWEEN_UPDATES) - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Get the latest data from luftdaten.info.""" from luftdaten.exceptions import LuftdatenError try: - yield from self.data.async_get_data() + await self.data.async_get_data() except LuftdatenError: _LOGGER.error("Unable to retrieve data from luftdaten.info") diff --git a/homeassistant/components/sensor/sabnzbd.py b/homeassistant/components/sensor/sabnzbd.py index 632e1ed5c1d..c5dd09e0ccc 100644 --- a/homeassistant/components/sensor/sabnzbd.py +++ b/homeassistant/components/sensor/sabnzbd.py @@ -75,15 +75,14 @@ def setup_sabnzbd(base_url, apikey, name, config, for variable in monitored]) -@asyncio.coroutine @Throttle(MIN_TIME_BETWEEN_UPDATES) -def async_update_queue(sab_api): +async def async_update_queue(sab_api): """ Throttled function to update SABnzbd queue. This ensures that the queue info only gets updated once for all sensors """ - yield from sab_api.refresh_queue() + await sab_api.refresh_queue() def request_configuration(host, name, hass, config, async_add_devices, diff --git a/homeassistant/components/sensor/startca.py b/homeassistant/components/sensor/startca.py index a5908812b6c..aefbc2d4626 100644 --- a/homeassistant/components/sensor/startca.py +++ b/homeassistant/components/sensor/startca.py @@ -140,21 +140,20 @@ class StartcaData(object): """ return float(value) * 10 ** -9 - @asyncio.coroutine @Throttle(MIN_TIME_BETWEEN_UPDATES) - def async_update(self): + async def async_update(self): """Get the Start.ca bandwidth data from the web service.""" import xmltodict _LOGGER.debug("Updating Start.ca usage data") url = 'https://www.start.ca/support/usage/api?key=' + \ self.api_key with async_timeout.timeout(REQUEST_TIMEOUT, loop=self.loop): - req = yield from self.websession.get(url) + req = await self.websession.get(url) if req.status != 200: _LOGGER.error("Request failed with status: %u", req.status) return False - data = yield from req.text() + data = await req.text() try: xml_data = xmltodict.parse(data) except ExpatError: diff --git a/homeassistant/components/sensor/teksavvy.py b/homeassistant/components/sensor/teksavvy.py index 9c4263422ff..0bf1ef4caff 100644 --- a/homeassistant/components/sensor/teksavvy.py +++ b/homeassistant/components/sensor/teksavvy.py @@ -132,22 +132,21 @@ class TekSavvyData(object): self.data = {"limit": self.bandwidth_cap} if self.bandwidth_cap > 0 \ else {"limit": float('inf')} - @asyncio.coroutine @Throttle(MIN_TIME_BETWEEN_UPDATES) - def async_update(self): + async def async_update(self): """Get the TekSavvy bandwidth data from the web service.""" headers = {"TekSavvy-APIKey": self.api_key} _LOGGER.debug("Updating TekSavvy data") url = "https://api.teksavvy.com/"\ "web/Usage/UsageSummaryRecords?$filter=IsCurrent%20eq%20true" with async_timeout.timeout(REQUEST_TIMEOUT, loop=self.loop): - req = yield from self.websession.get(url, headers=headers) + req = await self.websession.get(url, headers=headers) if req.status != 200: _LOGGER.error("Request failed with status: %u", req.status) return False try: - data = yield from req.json() + data = await req.json() for (api, ha_name) in API_HA_MAP: self.data[ha_name] = float(data["value"][0][api]) on_peak_download = self.data["onpeak_download"] diff --git a/homeassistant/components/sensor/wunderground.py b/homeassistant/components/sensor/wunderground.py index edcc1c92bf9..0375bb1344c 100644 --- a/homeassistant/components/sensor/wunderground.py +++ b/homeassistant/components/sensor/wunderground.py @@ -777,14 +777,13 @@ class WUndergroundData(object): return url + '.json' - @asyncio.coroutine @Throttle(MIN_TIME_BETWEEN_UPDATES) - def async_update(self): + async def async_update(self): """Get the latest data from WUnderground.""" try: with async_timeout.timeout(10, loop=self._hass.loop): - response = yield from self._session.get(self._build_url()) - result = yield from response.json() + response = await self._session.get(self._build_url()) + result = await response.json() if "error" in result['response']: raise ValueError(result['response']["error"]["description"]) self.data = result diff --git a/homeassistant/util/__init__.py b/homeassistant/util/__init__.py index 75721a37466..a869251dc3c 100644 --- a/homeassistant/util/__init__.py +++ b/homeassistant/util/__init__.py @@ -1,4 +1,5 @@ """Helper methods for various modules.""" +import asyncio from collections.abc import MutableSet from itertools import chain import threading @@ -276,6 +277,16 @@ class Throttle(object): is_func = (not hasattr(method, '__self__') and '.' not in method.__qualname__.split('..')[-1]) + # Make sure we return a coroutine if the method is async. + if asyncio.iscoroutinefunction(method): + async def throttled_value(): + """Stand-in function for when real func is being throttled.""" + return None + else: + def throttled_value(): + """Stand-in function for when real func is being throttled.""" + return None + @wraps(method) def wrapper(*args, **kwargs): """Wrap that allows wrapped to be called only once per min_time. @@ -298,7 +309,7 @@ class Throttle(object): throttle = host._throttle[id(self)] if not throttle[0].acquire(False): - return None + return throttled_value() # Check if method is never called or no_throttle is given force = kwargs.pop('no_throttle', False) or not throttle[1] @@ -309,7 +320,7 @@ class Throttle(object): throttle[1] = utcnow() return result - return None + return throttled_value() finally: throttle[0].release() diff --git a/tests/util/test_init.py b/tests/util/test_init.py index 2902cb62517..5493843c246 100644 --- a/tests/util/test_init.py +++ b/tests/util/test_init.py @@ -280,3 +280,14 @@ class TestUtil(unittest.TestCase): mock_random.SystemRandom.return_value = generator assert util.get_random_string(length=3) == 'ABC' + + +async def test_throttle_async(): + """Test Throttle decorator with async method.""" + @util.Throttle(timedelta(seconds=2)) + async def test_method(): + """Only first call should return a value.""" + return True + + assert (await test_method()) is True + assert (await test_method()) is None From a8a895a61b595ff371f8f0a6a93c9f78c647e6d3 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 9 Mar 2018 19:39:50 -0800 Subject: [PATCH 014/220] allow ios device tracker see calls to go through (#13020) --- .../components/device_tracker/__init__.py | 9 ++++++++- tests/components/device_tracker/test_init.py | 15 +++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index 196c11a614f..9fea2bc104d 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -111,6 +111,9 @@ SERVICE_SEE_PAYLOAD_SCHEMA = vol.Schema(vol.All( ATTR_ATTRIBUTES: dict, ATTR_SOURCE_TYPE: vol.In(SOURCE_TYPES), ATTR_CONSIDER_HOME: cv.time_period, + # Temp workaround for iOS app introduced in 0.65 + vol.Optional('battery_status'): str, + vol.Optional('hostname'): str, })) @@ -219,7 +222,11 @@ def async_setup(hass: HomeAssistantType, config: ConfigType): @asyncio.coroutine def async_see_service(call): """Service to see a device.""" - yield from tracker.async_see(**call.data) + # Temp workaround for iOS, introduced in 0.65 + data = dict(call.data) + data.pop('hostname', None) + data.pop('battery_status', None) + yield from tracker.async_see(**data) hass.services.async_register( DOMAIN, SERVICE_SEE, async_see_service, SERVICE_SEE_PAYLOAD_SCHEMA) diff --git a/tests/components/device_tracker/test_init.py b/tests/components/device_tracker/test_init.py index ebf568309ad..9d122fa17b6 100644 --- a/tests/components/device_tracker/test_init.py +++ b/tests/components/device_tracker/test_init.py @@ -730,3 +730,18 @@ async def test_old_style_track_new_is_skipped(mock_device_tracker_conf, hass): await hass.async_block_till_done() assert len(mock_device_tracker_conf) == 1 assert mock_device_tracker_conf[0].track is False + + +def test_see_schema_allowing_ios_calls(): + """Test SEE service schema allows extra keys. + + Temp work around because the iOS app sends incorrect data. + """ + device_tracker.SERVICE_SEE_PAYLOAD_SCHEMA({ + 'dev_id': 'Test', + "battery": 35, + "battery_status": 'Unplugged', + "gps": [10.0, 10.0], + "gps_accuracy": 300, + "hostname": 'beer', + }) From 3ca139e21e7af7ea30286ab4094f6c52efcb70b5 Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Sat, 10 Mar 2018 04:41:59 +0100 Subject: [PATCH 015/220] HomeKit Bugfix: names (#13031) * Fix display_names, changed default port (+1) * Revert port change --- .../components/homekit/accessories.py | 9 +++++---- homeassistant/components/homekit/const.py | 1 + tests/components/homekit/test_accessories.py | 20 ++++++++++--------- 3 files changed, 17 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index 689bcb3377c..1cd94070289 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -5,16 +5,17 @@ from pyhap.accessory import Accessory, Bridge, Category from .const import ( SERV_ACCESSORY_INFO, SERV_BRIDGING_STATE, MANUFACTURER, - CHAR_MODEL, CHAR_MANUFACTURER, CHAR_SERIAL_NUMBER) + CHAR_MODEL, CHAR_MANUFACTURER, CHAR_NAME, CHAR_SERIAL_NUMBER) _LOGGER = logging.getLogger(__name__) -def set_accessory_info(acc, model, manufacturer=MANUFACTURER, +def set_accessory_info(acc, name, model, manufacturer=MANUFACTURER, serial_number='0000'): """Set the default accessory information.""" service = acc.get_service(SERV_ACCESSORY_INFO) + service.get_characteristic(CHAR_NAME).set_value(name) service.get_characteristic(CHAR_MODEL).set_value(model) service.get_characteristic(CHAR_MANUFACTURER).set_value(manufacturer) service.get_characteristic(CHAR_SERIAL_NUMBER).set_value(serial_number) @@ -49,7 +50,7 @@ class HomeAccessory(Accessory): def __init__(self, display_name, model, category='OTHER', **kwargs): """Initialize a Accessory object.""" super().__init__(display_name, **kwargs) - set_accessory_info(self, model) + set_accessory_info(self, display_name, model) self.category = getattr(Category, category, Category.OTHER) def _set_services(self): @@ -62,7 +63,7 @@ class HomeBridge(Bridge): def __init__(self, display_name, model, pincode, **kwargs): """Initialize a Bridge object.""" super().__init__(display_name, pincode=pincode, **kwargs) - set_accessory_info(self, model) + set_accessory_info(self, display_name, model) def _set_services(self): add_preload_service(self, SERV_ACCESSORY_INFO) diff --git a/homeassistant/components/homekit/const.py b/homeassistant/components/homekit/const.py index 35bd25eabd3..73dfbf69049 100644 --- a/homeassistant/components/homekit/const.py +++ b/homeassistant/components/homekit/const.py @@ -22,6 +22,7 @@ CHAR_HEATING_THRESHOLD_TEMPERATURE = 'HeatingThresholdTemperature' CHAR_LINK_QUALITY = 'LinkQuality' CHAR_MANUFACTURER = 'Manufacturer' CHAR_MODEL = 'Model' +CHAR_NAME = 'Name' CHAR_ON = 'On' CHAR_POSITION_STATE = 'PositionState' CHAR_REACHABLE = 'Reachable' diff --git a/tests/components/homekit/test_accessories.py b/tests/components/homekit/test_accessories.py index a45aa82d981..6f39a8c792b 100644 --- a/tests/components/homekit/test_accessories.py +++ b/tests/components/homekit/test_accessories.py @@ -13,7 +13,7 @@ from homeassistant.components.homekit.accessories import ( HomeAccessory, HomeBridge) from homeassistant.components.homekit.const import ( SERV_ACCESSORY_INFO, SERV_BRIDGING_STATE, - CHAR_MODEL, CHAR_MANUFACTURER, CHAR_SERIAL_NUMBER) + CHAR_MODEL, CHAR_MANUFACTURER, CHAR_NAME, CHAR_SERIAL_NUMBER) from tests.mock.homekit import ( get_patch_paths, mock_preload_service, @@ -69,21 +69,23 @@ def test_override_properties(): def test_set_accessory_info(): """Test setting of basic accessory information with MockAccessory.""" acc = MockAccessory('Accessory') - set_accessory_info(acc, 'model', 'manufacturer', '0000') + set_accessory_info(acc, 'name', 'model', 'manufacturer', '0000') assert len(acc.services) == 1 serv = acc.services[0] assert serv.display_name == SERV_ACCESSORY_INFO - assert len(serv.characteristics) == 3 + assert len(serv.characteristics) == 4 chars = serv.characteristics - assert chars[0].display_name == CHAR_MODEL - assert chars[0].value == 'model' - assert chars[1].display_name == CHAR_MANUFACTURER - assert chars[1].value == 'manufacturer' - assert chars[2].display_name == CHAR_SERIAL_NUMBER - assert chars[2].value == '0000' + assert chars[0].display_name == CHAR_NAME + assert chars[0].value == 'name' + assert chars[1].display_name == CHAR_MODEL + assert chars[1].value == 'model' + assert chars[2].display_name == CHAR_MANUFACTURER + assert chars[2].value == 'manufacturer' + assert chars[3].display_name == CHAR_SERIAL_NUMBER + assert chars[3].value == '0000' @patch(PATH_ACC, side_effect=mock_preload_service) From 2fae86bbd33c2d196843d535e28dde9478c00b97 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Sat, 10 Mar 2018 08:39:49 +0100 Subject: [PATCH 016/220] Make lazytox script executable (#13040) --- script/lazytox.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 script/lazytox.py diff --git a/script/lazytox.py b/script/lazytox.py old mode 100644 new mode 100755 From 76fb2447a556b33e535d0468bcb5a021f16d039b Mon Sep 17 00:00:00 2001 From: Jerad Meisner Date: Sat, 10 Mar 2018 00:27:13 -0800 Subject: [PATCH 017/220] Bump pysabnzbd version (#13042) --- homeassistant/components/sensor/sabnzbd.py | 4 ++-- requirements_all.txt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sensor/sabnzbd.py b/homeassistant/components/sensor/sabnzbd.py index c5dd09e0ccc..194ff71222a 100644 --- a/homeassistant/components/sensor/sabnzbd.py +++ b/homeassistant/components/sensor/sabnzbd.py @@ -19,7 +19,7 @@ from homeassistant.util import Throttle from homeassistant.util.json import load_json, save_json import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['pysabnzbd==0.0.3'] +REQUIREMENTS = ['pysabnzbd==1.0.1'] _CONFIGURING = {} _LOGGER = logging.getLogger(__name__) @@ -82,7 +82,7 @@ async def async_update_queue(sab_api): This ensures that the queue info only gets updated once for all sensors """ - await sab_api.refresh_queue() + await sab_api.refresh_data() def request_configuration(host, name, hass, config, async_add_devices, diff --git a/requirements_all.txt b/requirements_all.txt index cdf627b8507..b71f8c902a2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -863,7 +863,7 @@ pyqwikswitch==0.4 pyrainbird==0.1.3 # homeassistant.components.sensor.sabnzbd -pysabnzbd==0.0.3 +pysabnzbd==1.0.1 # homeassistant.components.climate.sensibo pysensibo==1.0.2 From f9c1675c95d5462d2ca98adabdd7767378527919 Mon Sep 17 00:00:00 2001 From: Paul Tarjan Date: Sat, 10 Mar 2018 00:36:20 -0800 Subject: [PATCH 018/220] Use request.query (#13037) Fixes #13036 --- homeassistant/components/wink/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/wink/__init__.py b/homeassistant/components/wink/__init__.py index 78f3042aefb..eab67c18aed 100644 --- a/homeassistant/components/wink/__init__.py +++ b/homeassistant/components/wink/__init__.py @@ -554,7 +554,7 @@ class WinkAuthCallbackView(HomeAssistantView): from aiohttp import web hass = request.app['hass'] - data = request.GET + data = request.query response_message = """Wink has been successfully authorized! You can close this window now! For the best results you should reboot From 4d74fc2d076282d4f07a98096a99acb23311779d Mon Sep 17 00:00:00 2001 From: John Allen Date: Sat, 10 Mar 2018 03:52:45 -0500 Subject: [PATCH 019/220] Fix sensibo's min/max_temp properties (#12996) The super class has these as properties, not regular methods --- homeassistant/components/climate/sensibo.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/climate/sensibo.py b/homeassistant/components/climate/sensibo.py index 7b4c8ed8b1b..68b5eee35ef 100644 --- a/homeassistant/components/climate/sensibo.py +++ b/homeassistant/components/climate/sensibo.py @@ -240,13 +240,13 @@ class SensiboClimate(ClimateDevice): def min_temp(self): """Return the minimum temperature.""" return self._temperatures_list[0] \ - if self._temperatures_list else super().min_temp() + if self._temperatures_list else super().min_temp @property def max_temp(self): """Return the maximum temperature.""" return self._temperatures_list[-1] \ - if self._temperatures_list else super().max_temp() + if self._temperatures_list else super().max_temp @asyncio.coroutine def async_set_temperature(self, **kwargs): From e910ecfd5f642c80161c0bf243c06e29fd6a1913 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Sat, 10 Mar 2018 11:07:02 +0100 Subject: [PATCH 020/220] Fix async lifx_set_state (#13045) --- homeassistant/components/light/lifx.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/light/lifx.py b/homeassistant/components/light/lifx.py index 18bc39d88d2..0bb65a78c6e 100644 --- a/homeassistant/components/light/lifx.py +++ b/homeassistant/components/light/lifx.py @@ -221,7 +221,7 @@ class LIFXManager(object): tasks = [] for light in self.service_to_entities(service): if service.service == SERVICE_LIFX_SET_STATE: - task = light.async_set_state(**service.data) + task = light.set_state(**service.data) tasks.append(self.hass.async_add_job(task)) if tasks: await asyncio.wait(tasks, loop=self.hass.loop) From 0143752d943a4c7b63fb87e5412efdbb879cdeb8 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Sat, 10 Mar 2018 18:10:50 +0100 Subject: [PATCH 021/220] Yeelight version bumped. (#13056) --- homeassistant/components/light/yeelight.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/light/yeelight.py b/homeassistant/components/light/yeelight.py index 33c84df14be..ca10d246ce8 100644 --- a/homeassistant/components/light/yeelight.py +++ b/homeassistant/components/light/yeelight.py @@ -26,7 +26,7 @@ from homeassistant.components.light import ( Light, PLATFORM_SCHEMA) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['yeelight==0.3.3'] +REQUIREMENTS = ['yeelight==0.4.0'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index b71f8c902a2..a4c866e79df 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1292,7 +1292,7 @@ yahoo-finance==1.4.0 yahooweather==0.10 # homeassistant.components.light.yeelight -yeelight==0.3.3 +yeelight==0.4.0 # homeassistant.components.light.yeelightsunflower yeelightsunflower==0.0.8 From b4b779c49de8790c5328dd6bacd45e79e3cfe0d2 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Sat, 10 Mar 2018 18:11:10 +0100 Subject: [PATCH 022/220] python-miio version bumped. (#13055) --- homeassistant/components/fan/xiaomi_miio.py | 2 +- homeassistant/components/light/xiaomi_miio.py | 2 +- homeassistant/components/remote/xiaomi_miio.py | 2 +- homeassistant/components/switch/xiaomi_miio.py | 2 +- homeassistant/components/vacuum/xiaomi_miio.py | 2 +- requirements_all.txt | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/fan/xiaomi_miio.py b/homeassistant/components/fan/xiaomi_miio.py index b9bc54b5c79..09df55200a2 100644 --- a/homeassistant/components/fan/xiaomi_miio.py +++ b/homeassistant/components/fan/xiaomi_miio.py @@ -29,7 +29,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, }) -REQUIREMENTS = ['python-miio==0.3.7'] +REQUIREMENTS = ['python-miio==0.3.8'] ATTR_TEMPERATURE = 'temperature' ATTR_HUMIDITY = 'humidity' diff --git a/homeassistant/components/light/xiaomi_miio.py b/homeassistant/components/light/xiaomi_miio.py index d9b7d6c76db..77b02600f33 100644 --- a/homeassistant/components/light/xiaomi_miio.py +++ b/homeassistant/components/light/xiaomi_miio.py @@ -40,7 +40,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ 'philips.light.bulb']), }) -REQUIREMENTS = ['python-miio==0.3.7'] +REQUIREMENTS = ['python-miio==0.3.8'] # The light does not accept cct values < 1 CCT_MIN = 1 diff --git a/homeassistant/components/remote/xiaomi_miio.py b/homeassistant/components/remote/xiaomi_miio.py index 924556a039d..30141eaf5e6 100644 --- a/homeassistant/components/remote/xiaomi_miio.py +++ b/homeassistant/components/remote/xiaomi_miio.py @@ -21,7 +21,7 @@ from homeassistant.const import ( import homeassistant.helpers.config_validation as cv from homeassistant.util.dt import utcnow -REQUIREMENTS = ['python-miio==0.3.7'] +REQUIREMENTS = ['python-miio==0.3.8'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/switch/xiaomi_miio.py b/homeassistant/components/switch/xiaomi_miio.py index ae4329a42a1..1a8feb5811d 100644 --- a/homeassistant/components/switch/xiaomi_miio.py +++ b/homeassistant/components/switch/xiaomi_miio.py @@ -33,7 +33,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ 'chuangmi.plug.v2']), }) -REQUIREMENTS = ['python-miio==0.3.7'] +REQUIREMENTS = ['python-miio==0.3.8'] ATTR_POWER = 'power' ATTR_TEMPERATURE = 'temperature' diff --git a/homeassistant/components/vacuum/xiaomi_miio.py b/homeassistant/components/vacuum/xiaomi_miio.py index 1d4ab5eb7ca..f42a895f94f 100644 --- a/homeassistant/components/vacuum/xiaomi_miio.py +++ b/homeassistant/components/vacuum/xiaomi_miio.py @@ -19,7 +19,7 @@ from homeassistant.const import ( ATTR_ENTITY_ID, CONF_HOST, CONF_NAME, CONF_TOKEN, STATE_OFF, STATE_ON) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['python-miio==0.3.7'] +REQUIREMENTS = ['python-miio==0.3.8'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index a4c866e79df..218a2dec9d2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -938,7 +938,7 @@ python-juicenet==0.0.5 # homeassistant.components.remote.xiaomi_miio # homeassistant.components.switch.xiaomi_miio # homeassistant.components.vacuum.xiaomi_miio -python-miio==0.3.7 +python-miio==0.3.8 # homeassistant.components.media_player.mpd python-mpd2==0.5.5 From 86baed4e52470a86b29912b02fb6ef3c3873518a Mon Sep 17 00:00:00 2001 From: Timmo <28114703+timmo001@users.noreply.github.com> Date: Sat, 10 Mar 2018 17:11:53 +0000 Subject: [PATCH 023/220] Glances Docker Sensors (#13026) * Added container count * Added container count * Change Name * Fix if * Added Docker cpu use and memory use --- homeassistant/components/sensor/glances.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/homeassistant/components/sensor/glances.py b/homeassistant/components/sensor/glances.py index b61b7abeae3..3b6f3ddc99d 100644 --- a/homeassistant/components/sensor/glances.py +++ b/homeassistant/components/sensor/glances.py @@ -42,6 +42,9 @@ SENSOR_TYPES = { 'process_thread': ['Thread', 'Count', 'mdi:memory'], 'process_sleeping': ['Sleeping', 'Count', 'mdi:memory'], 'cpu_temp': ['CPU Temp', TEMP_CELSIUS, 'mdi:thermometer'], + 'docker_active': ['Containers active', '', 'mdi:docker'], + 'docker_cpu_use': ['Containers CPU used', '%', 'mdi:docker'], + 'docker_memory_use': ['Containers RAM used', 'MiB', 'mdi:docker'], } PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ @@ -155,6 +158,22 @@ class GlancesSensor(Entity): if sensor['label'] == 'CPU': self._state = sensor['value'] self._state = None + elif self.type == 'docker_active': + count = 0 + for container in value['docker']['containers']: + if container['Status'] == 'running': + count += 1 + self._state = count + elif self.type == 'docker_cpu_use': + use = 0.0 + for container in value['docker']['containers']: + use += container['cpu']['total'] + self._state = round(use, 1) + elif self.type == 'docker_memory_use': + use = 0.0 + for container in value['docker']['containers']: + use += container['memory']['usage'] + self._state = round(use / 1024**2, 1) class GlancesData(object): From 7ea7fc8d38c7334a191f5c3fe718b88c27580d6a Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Sat, 10 Mar 2018 18:12:23 +0100 Subject: [PATCH 024/220] Script/lint, Lazytox: Fix issue to ignore delete files (#13051) * Fix issue to ignore delete files * Updated lazytox --- script/lazytox.py | 4 +++- script/lint | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/script/lazytox.py b/script/lazytox.py index 2137ae1794c..7c57b49c00d 100755 --- a/script/lazytox.py +++ b/script/lazytox.py @@ -127,6 +127,7 @@ async def flake8(files): async def lint(files): """Perform lint.""" + files = [file for file in files if os.path.isfile(file)] fres, pres = await asyncio.gather(flake8(files), pylint(files)) res = fres + pres @@ -182,7 +183,8 @@ async def main(): gen_req = True # requirements script for components # Find test files... if fname.startswith('tests/'): - if '/test_' in fname: # All test helpers should be excluded + if '/test_' in fname and os.path.isfile(fname): + # All test helpers should be excluded test_files.add(fname) else: parts = fname.split('/') diff --git a/script/lint b/script/lint index 9d994429f74..44871dbc9a4 100755 --- a/script/lint +++ b/script/lint @@ -3,9 +3,9 @@ cd "$(dirname "$0")/.." -export files="`git diff upstream/dev... --name-only | grep -e '\.py$'`" +export files="`git diff upstream/dev... --diff-filter=d --name-only | grep -e '\.py$'`" echo "=================================================" -echo "FILES CHANGED (git diff upstream/dev... --name-only)" +echo "FILES CHANGED (git diff upstream/dev... --diff-filter=d --name-only)" echo "=================================================" if [ -z "$files" ] ; then echo "No python file changed. Rather use: tox -e lint" From 40485a6e893a6ebb460b41a69b6484693d438b4c Mon Sep 17 00:00:00 2001 From: Johann Kellerman Date: Sat, 10 Mar 2018 20:02:04 +0200 Subject: [PATCH 025/220] Ensure we have valid config AFTER merging packages #13015 (#13038) * Ensure we have valid config AFTER merging packages #13015 * also fix packages --- homeassistant/bootstrap.py | 10 ++++++---- homeassistant/config.py | 3 +++ homeassistant/scripts/check_config.py | 5 +++++ 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 50d8502bbd1..34eab679581 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -113,15 +113,17 @@ def async_from_config_dict(config: Dict[str, Any], yield from hass.async_add_job(loader.prepare, hass) # Make a copy because we are mutating it. - new_config = OrderedDict() - for key, value in config.items(): - new_config[key] = value or {} - config = new_config + config = OrderedDict(config) # Merge packages conf_util.merge_packages_config( config, core_config.get(conf_util.CONF_PACKAGES, {})) + # Ensure we have no None values after merge + for key, value in config.items(): + if not value: + config[key] = {} + hass.config_entries = config_entries.ConfigEntries(hass, config) yield from hass.config_entries.async_load() diff --git a/homeassistant/config.py b/homeassistant/config.py index 5f2c6cf1625..e94fc297f48 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -562,6 +562,9 @@ def merge_packages_config(config, packages, _log_pkg_error=_log_pkg_error): continue if merge_type == 'dict': + if comp_conf is None: + comp_conf = OrderedDict() + if not isinstance(comp_conf, dict): _log_pkg_error( pack_name, comp_name, config, diff --git a/homeassistant/scripts/check_config.py b/homeassistant/scripts/check_config.py index 4e80b3c6536..641693501ff 100644 --- a/homeassistant/scripts/check_config.py +++ b/homeassistant/scripts/check_config.py @@ -326,6 +326,11 @@ def check_ha_config_file(config_dir): config, core_config.get(CONF_PACKAGES, {}), _pack_error) del core_config[CONF_PACKAGES] + # Ensure we have no None values after merge + for key, value in config.items(): + if not value: + config[key] = {} + # Filter out repeating config sections components = set(key.split(' ')[0] for key in config.keys()) From f01b5b00404a8edb6e3f268003cce3be4cca332e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 10 Mar 2018 10:02:16 -0800 Subject: [PATCH 026/220] Don't call async from sync (#13057) --- homeassistant/components/xiaomi_aqara.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/xiaomi_aqara.py b/homeassistant/components/xiaomi_aqara.py index b6e04d867fa..244605a7b97 100644 --- a/homeassistant/components/xiaomi_aqara.py +++ b/homeassistant/components/xiaomi_aqara.py @@ -182,19 +182,19 @@ def setup(hass, config): gateway_only_schema = _add_gateway_to_schema(xiaomi, vol.Schema({})) - hass.services.async_register( + hass.services.register( DOMAIN, SERVICE_PLAY_RINGTONE, play_ringtone_service, schema=_add_gateway_to_schema(xiaomi, SERVICE_SCHEMA_PLAY_RINGTONE)) - hass.services.async_register( + hass.services.register( DOMAIN, SERVICE_STOP_RINGTONE, stop_ringtone_service, schema=gateway_only_schema) - hass.services.async_register( + hass.services.register( DOMAIN, SERVICE_ADD_DEVICE, add_device_service, schema=gateway_only_schema) - hass.services.async_register( + hass.services.register( DOMAIN, SERVICE_REMOVE_DEVICE, remove_device_service, schema=_add_gateway_to_schema(xiaomi, SERVICE_SCHEMA_REMOVE_DEVICE)) From ae47da7bce8c2a58c14fa65c731a0a22184a715e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 10 Mar 2018 10:19:49 -0800 Subject: [PATCH 027/220] Update frontend to 20180310.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 501537e3ed3..1f5a7576302 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -24,7 +24,7 @@ from homeassistant.core import callback from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass -REQUIREMENTS = ['home-assistant-frontend==20180309.0'] +REQUIREMENTS = ['home-assistant-frontend==20180310.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index 218a2dec9d2..df5f3387d46 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -353,7 +353,7 @@ hipnotify==1.0.8 holidays==0.9.3 # homeassistant.components.frontend -home-assistant-frontend==20180309.0 +home-assistant-frontend==20180310.0 # homeassistant.components.camera.onvif http://github.com/tgaugry/suds-passworddigest-py3/archive/86fc50e39b4d2b8997481967d6a7fe1c57118999.zip#suds-passworddigest-py3==0.1.2a diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 65e94172553..460e70cbca5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -78,7 +78,7 @@ hbmqtt==0.9.1 holidays==0.9.3 # homeassistant.components.frontend -home-assistant-frontend==20180309.0 +home-assistant-frontend==20180310.0 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From 0e00de8a332045ee9e722615253318edbe592f58 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 10 Mar 2018 10:40:28 -0800 Subject: [PATCH 028/220] Convert decimals from SQL results (#13059) --- homeassistant/components/sensor/sql.py | 8 ++++++-- tests/components/sensor/test_sql.py | 5 +++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/sensor/sql.py b/homeassistant/components/sensor/sql.py index 50d60bfc426..402076c6fd8 100644 --- a/homeassistant/components/sensor/sql.py +++ b/homeassistant/components/sensor/sql.py @@ -4,6 +4,7 @@ Sensor from an SQL Query. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.sql/ """ +import decimal import logging import voluptuous as vol @@ -131,17 +132,20 @@ class SQLSensor(Entity): try: sess = self.sessionmaker() result = sess.execute(self._query) + self._attributes = {} if not result.returns_rows or result.rowcount == 0: _LOGGER.warning("%s returned no results", self._query) self._state = None - self._attributes = {} return for res in result: _LOGGER.debug("result = %s", res.items()) data = res[self._column_name] - self._attributes = {k: v for k, v in res.items()} + for key, value in res.items(): + if isinstance(value, decimal.Decimal): + value = float(decimal) + self._attributes[key] = value except sqlalchemy.exc.SQLAlchemyError as err: _LOGGER.error("Error executing query %s: %s", self._query, err) return diff --git a/tests/components/sensor/test_sql.py b/tests/components/sensor/test_sql.py index 5e639b9f338..7665b5c9037 100644 --- a/tests/components/sensor/test_sql.py +++ b/tests/components/sensor/test_sql.py @@ -29,7 +29,7 @@ class TestSQLSensor(unittest.TestCase): 'db_url': 'sqlite://', 'queries': [{ 'name': 'count_tables', - 'query': 'SELECT count(*) value FROM sqlite_master;', + 'query': 'SELECT 5 as value', 'column': 'value', }] } @@ -38,7 +38,8 @@ class TestSQLSensor(unittest.TestCase): assert setup_component(self.hass, 'sensor', config) state = self.hass.states.get('sensor.count_tables') - self.assertEqual(state.state, '0') + assert state.state == '5' + assert state.attributes['value'] == 5 def test_invalid_query(self): """Test the SQL sensor for invalid queries.""" From 28ff1f7ac2b72d8d11486ab00016c5dc3a8ca15f Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Sat, 10 Mar 2018 22:27:58 +0000 Subject: [PATCH 029/220] Convert decimals from SQL results --- homeassistant/components/sensor/sql.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/sql.py b/homeassistant/components/sensor/sql.py index 402076c6fd8..5d5d61ff822 100644 --- a/homeassistant/components/sensor/sql.py +++ b/homeassistant/components/sensor/sql.py @@ -144,7 +144,7 @@ class SQLSensor(Entity): data = res[self._column_name] for key, value in res.items(): if isinstance(value, decimal.Decimal): - value = float(decimal) + value = float(value) self._attributes[key] = value except sqlalchemy.exc.SQLAlchemyError as err: _LOGGER.error("Error executing query %s: %s", self._query, err) From 3f6d30ed0615bfcab69c0683cb59f865abb3d0b1 Mon Sep 17 00:00:00 2001 From: Julius Mittenzwei Date: Sun, 11 Mar 2018 05:26:21 +0100 Subject: [PATCH 030/220] Fixes KNX fire event problem, issue https://github.com/home-assistant/home-assistant/issues/13049 (#13062) --- homeassistant/components/knx.py | 4 ++-- requirements_all.txt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/knx.py b/homeassistant/components/knx.py index f6f41619ca8..61f8ca90137 100644 --- a/homeassistant/components/knx.py +++ b/homeassistant/components/knx.py @@ -17,7 +17,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import async_track_state_change from homeassistant.helpers.script import Script -REQUIREMENTS = ['xknx==0.8.4'] +REQUIREMENTS = ['xknx==0.8.5'] DOMAIN = "knx" DATA_KNX = "data_knx" @@ -241,7 +241,7 @@ class KNXModule(object): async def telegram_received_cb(self, telegram): """Call invoked after a KNX telegram was received.""" self.hass.bus.fire('knx_event', { - 'address': telegram.group_address.str(), + 'address': str(telegram.group_address), 'data': telegram.payload.value }) # False signals XKNX to proceed with processing telegrams. diff --git a/requirements_all.txt b/requirements_all.txt index df5f3387d46..5bc5975aa29 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1274,7 +1274,7 @@ xbee-helper==0.0.7 xboxapi==0.1.1 # homeassistant.components.knx -xknx==0.8.4 +xknx==0.8.5 # homeassistant.components.media_player.bluesound # homeassistant.components.sensor.startca From 458598546d7992759a8c922f6dd82276d4981fb2 Mon Sep 17 00:00:00 2001 From: Jesse Hills Date: Sun, 11 Mar 2018 17:31:57 +1300 Subject: [PATCH 031/220] - Bump iGlo Version (#13063) - Use effect list as a method --- homeassistant/components/light/iglo.py | 4 ++-- requirements_all.txt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/light/iglo.py b/homeassistant/components/light/iglo.py index 1e110b5c397..9717993f77d 100644 --- a/homeassistant/components/light/iglo.py +++ b/homeassistant/components/light/iglo.py @@ -17,7 +17,7 @@ from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT import homeassistant.helpers.config_validation as cv import homeassistant.util.color as color_util -REQUIREMENTS = ['iglo==1.2.6'] +REQUIREMENTS = ['iglo==1.2.7'] _LOGGER = logging.getLogger(__name__) @@ -89,7 +89,7 @@ class IGloLamp(Light): @property def effect_list(self): """Return the list of supported effects.""" - return self._lamp.effect_list + return self._lamp.effect_list() @property def supported_features(self): diff --git a/requirements_all.txt b/requirements_all.txt index 5bc5975aa29..07a967a181f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -398,7 +398,7 @@ https://github.com/wokar/pylgnetcast/archive/v0.2.0.zip#pylgnetcast==0.2.0 # i2csense==0.0.4 # homeassistant.components.light.iglo -iglo==1.2.6 +iglo==1.2.7 # homeassistant.components.ihc ihcsdk==2.2.0 From d74a2b68c137f549c196a6634773f9027a4596c2 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Sun, 11 Mar 2018 05:45:32 +0100 Subject: [PATCH 032/220] Sensor template don't exit early on TemplateError (#13041) * Sensor template don't exit early on TemplateError * Add friendly name unknown state test * Also track entites from attribute templates * Use set instead of list --- homeassistant/components/sensor/template.py | 41 +++++++++++++-------- tests/components/sensor/test_template.py | 27 ++++++++++++++ 2 files changed, 53 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/sensor/template.py b/homeassistant/components/sensor/template.py index 582bc3a0150..1cd43262513 100644 --- a/homeassistant/components/sensor/template.py +++ b/homeassistant/components/sensor/template.py @@ -14,7 +14,8 @@ from homeassistant.components.sensor import ENTITY_ID_FORMAT, PLATFORM_SCHEMA from homeassistant.const import ( ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT, CONF_VALUE_TEMPLATE, CONF_ICON_TEMPLATE, CONF_ENTITY_PICTURE_TEMPLATE, ATTR_ENTITY_ID, - CONF_SENSORS, EVENT_HOMEASSISTANT_START, CONF_FRIENDLY_NAME_TEMPLATE) + CONF_SENSORS, EVENT_HOMEASSISTANT_START, CONF_FRIENDLY_NAME_TEMPLATE, + MATCH_ALL) from homeassistant.exceptions import TemplateError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity, async_generate_entity_id @@ -48,22 +49,32 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): icon_template = device_config.get(CONF_ICON_TEMPLATE) entity_picture_template = device_config.get( CONF_ENTITY_PICTURE_TEMPLATE) - entity_ids = (device_config.get(ATTR_ENTITY_ID) or - state_template.extract_entities()) friendly_name = device_config.get(ATTR_FRIENDLY_NAME, device) friendly_name_template = device_config.get(CONF_FRIENDLY_NAME_TEMPLATE) unit_of_measurement = device_config.get(ATTR_UNIT_OF_MEASUREMENT) - state_template.hass = hass + entity_ids = set() + manual_entity_ids = device_config.get(ATTR_ENTITY_ID) - if icon_template is not None: - icon_template.hass = hass + for template in (state_template, icon_template, + entity_picture_template, friendly_name_template): + if template is None: + continue + template.hass = hass - if entity_picture_template is not None: - entity_picture_template.hass = hass + if entity_ids == MATCH_ALL or manual_entity_ids is not None: + continue - if friendly_name_template is not None: - friendly_name_template.hass = hass + template_entity_ids = template.extract_entities() + if template_entity_ids == MATCH_ALL: + entity_ids = MATCH_ALL + else: + entity_ids |= set(template_entity_ids) + + if manual_entity_ids is not None: + entity_ids = manual_entity_ids + elif entity_ids != MATCH_ALL: + entity_ids = list(entity_ids) sensors.append( SensorTemplate( @@ -166,10 +177,10 @@ class SensorTemplate(Entity): # Common during HA startup - so just a warning _LOGGER.warning('Could not render template %s,' ' the state is unknown.', self._name) - return - self._state = None - _LOGGER.error('Could not render template %s: %s', self._name, ex) - + else: + self._state = None + _LOGGER.error('Could not render template %s: %s', self._name, + ex) for property_name, template in ( ('_icon', self._icon_template), ('_entity_picture', self._entity_picture_template), @@ -187,7 +198,7 @@ class SensorTemplate(Entity): _LOGGER.warning('Could not render %s template %s,' ' the state is unknown.', friendly_property_name, self._name) - return + continue try: setattr(self, property_name, diff --git a/tests/components/sensor/test_template.py b/tests/components/sensor/test_template.py index 5e258bc9245..b05fc90bfe4 100644 --- a/tests/components/sensor/test_template.py +++ b/tests/components/sensor/test_template.py @@ -131,6 +131,33 @@ class TestTemplateSensor: state = self.hass.states.get('sensor.test_template_sensor') assert state.attributes['friendly_name'] == 'It Works.' + def test_friendly_name_template_with_unknown_state(self): + """Test friendly_name template with an unknown value_template.""" + with assert_setup_component(1): + assert setup_component(self.hass, 'sensor', { + 'sensor': { + 'platform': 'template', + 'sensors': { + 'test_template_sensor': { + 'value_template': "{{ states.fourohfour.state }}", + 'friendly_name_template': + "It {{ states.sensor.test_state.state }}." + } + } + } + }) + + self.hass.start() + self.hass.block_till_done() + + state = self.hass.states.get('sensor.test_template_sensor') + assert state.attributes['friendly_name'] == 'It .' + + self.hass.states.set('sensor.test_state', 'Works') + self.hass.block_till_done() + state = self.hass.states.get('sensor.test_template_sensor') + assert state.attributes['friendly_name'] == 'It Works.' + def test_template_syntax_error(self): """Test templating syntax error.""" with assert_setup_component(0): From f164a5a65ffebacae2ec7cbf9d39f825fcdf2a8e Mon Sep 17 00:00:00 2001 From: Johann Kellerman Date: Sun, 11 Mar 2018 12:51:03 +0200 Subject: [PATCH 033/220] Better errors for unknown secrets (#13072) --- homeassistant/scripts/check_config.py | 6 +++++- homeassistant/util/yaml.py | 3 +-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/homeassistant/scripts/check_config.py b/homeassistant/scripts/check_config.py index 641693501ff..1a58757d17f 100644 --- a/homeassistant/scripts/check_config.py +++ b/homeassistant/scripts/check_config.py @@ -140,6 +140,9 @@ def run(script_args: List) -> int: print(color(C_HEAD, 'Used Secrets:')) for skey, sval in res['secrets'].items(): + if sval is None: + print(' -', skey + ':', color('red', "not found")) + continue print(' -', skey + ':', sval, color('cyan', '[from:', flatsecret .get(skey, 'keyring') + ']')) @@ -308,7 +311,8 @@ def check_ha_config_file(config_dir): return result.add_error("File configuration.yaml not found.") config = load_yaml_config_file(config_path) except HomeAssistantError as err: - return result.add_error(err) + return result.add_error( + "Error loading {}: {}".format(config_path, err)) finally: yaml.clear_secret_cache() diff --git a/homeassistant/util/yaml.py b/homeassistant/util/yaml.py index 8ac8d096b99..66d673987a3 100644 --- a/homeassistant/util/yaml.py +++ b/homeassistant/util/yaml.py @@ -288,8 +288,7 @@ def _secret_yaml(loader: SafeLineLoader, # Catch if package installed and no config credstash = None - _LOGGER.error("Secret %s not defined", node.value) - raise HomeAssistantError(node.value) + raise HomeAssistantError("Secret {} not defined".format(node.value)) yaml.SafeLoader.add_constructor('!include', _include_yaml) From dc8424032bf813fb597ab62baab852ce6535dda8 Mon Sep 17 00:00:00 2001 From: Adam Mills Date: Sun, 11 Mar 2018 10:30:03 -0400 Subject: [PATCH 034/220] Remove Z-Wave old/new entity_id attributes (#12652) --- homeassistant/components/zwave/__init__.py | 53 ++----------------- homeassistant/components/zwave/const.py | 1 - homeassistant/components/zwave/node_entity.py | 14 +---- tests/components/zwave/test_init.py | 37 ++++--------- tests/components/zwave/test_node_entity.py | 12 ++--- 5 files changed, 19 insertions(+), 98 deletions(-) diff --git a/homeassistant/components/zwave/__init__.py b/homeassistant/components/zwave/__init__.py index 21db39d4e76..ad4ae66df17 100644 --- a/homeassistant/components/zwave/__init__.py +++ b/homeassistant/components/zwave/__init__.py @@ -20,7 +20,7 @@ from homeassistant.const import ( ATTR_ENTITY_ID, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) from homeassistant.helpers.entity_values import EntityValues from homeassistant.helpers.event import track_time_change -from homeassistant.util import convert, slugify +from homeassistant.util import convert import homeassistant.util.dt as dt_util import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import ( @@ -52,7 +52,6 @@ CONF_DEVICE_CONFIG = 'device_config' CONF_DEVICE_CONFIG_GLOB = 'device_config_glob' CONF_DEVICE_CONFIG_DOMAIN = 'device_config_domain' CONF_NETWORK_KEY = 'network_key' -CONF_NEW_ENTITY_IDS = 'new_entity_ids' ATTR_POWER = 'power_consumption' @@ -161,7 +160,6 @@ CONFIG_SCHEMA = vol.Schema({ cv.positive_int, vol.Optional(CONF_USB_STICK_PATH, default=DEFAULT_CONF_USB_STICK_PATH): cv.string, - vol.Optional(CONF_NEW_ENTITY_IDS, default=True): cv.boolean, }), }, extra=vol.ALLOW_EXTRA) @@ -178,27 +176,6 @@ def _value_name(value): return '{} {}'.format(node_name(value.node), value.label).strip() -def _node_object_id(node): - """Return the object_id of the node.""" - node_object_id = '{}_{}'.format(slugify(node_name(node)), node.node_id) - return node_object_id - - -def object_id(value): - """Return the object_id of the device value. - - The object_id contains node_id and value instance id - to not collide with other entity_ids. - """ - _object_id = "{}_{}_{}".format(slugify(_value_name(value)), - value.node.node_id, value.index) - - # Add the instance id if there is more than one instance for the value - if value.instance > 1: - return '{}_{}'.format(_object_id, value.instance) - return _object_id - - def nice_print_node(node): """Print a nice formatted node to the output (debug method).""" node_dict = _obj_to_dict(node) @@ -260,13 +237,6 @@ def setup(hass, config): config[DOMAIN][CONF_DEVICE_CONFIG], config[DOMAIN][CONF_DEVICE_CONFIG_DOMAIN], config[DOMAIN][CONF_DEVICE_CONFIG_GLOB]) - new_entity_ids = config[DOMAIN][CONF_NEW_ENTITY_IDS] - if not new_entity_ids: - _LOGGER.warning( - "ZWave entity_ids will soon be changing. To opt in to new " - "entity_ids now, set `new_entity_ids: true` under zwave in your " - "configuration.yaml. See the following blog post for details: " - "https://home-assistant.io/blog/2017/06/15/zwave-entity-ids/") # Setup options options = ZWaveOption( @@ -328,12 +298,9 @@ def setup(hass, config): def node_added(node): """Handle a new node on the network.""" - entity = ZWaveNodeEntity(node, network, new_entity_ids) + entity = ZWaveNodeEntity(node, network) name = node_name(node) - if new_entity_ids: - generated_id = generate_entity_id(DOMAIN + '.{}', name, []) - else: - generated_id = entity.entity_id + generated_id = generate_entity_id(DOMAIN + '.{}', name, []) node_config = device_config.get(generated_id) if node_config.get(CONF_IGNORED): _LOGGER.info( @@ -794,11 +761,7 @@ class ZWaveDeviceEntityValues(): component = workaround_component value_name = _value_name(self.primary) - if self._zwave_config[DOMAIN][CONF_NEW_ENTITY_IDS]: - generated_id = generate_entity_id( - component + '.{}', value_name, []) - else: - generated_id = "{}.{}".format(component, object_id(self.primary)) + generated_id = generate_entity_id(component + '.{}', value_name, []) node_config = self._device_config.get(generated_id) # Configure node @@ -831,12 +794,6 @@ class ZWaveDeviceEntityValues(): self._workaround_ignore = True return - device.old_entity_id = "{}.{}".format( - component, object_id(self.primary)) - device.new_entity_id = "{}.{}".format(component, slugify(device.name)) - if not self._zwave_config[DOMAIN][CONF_NEW_ENTITY_IDS]: - device.entity_id = device.old_entity_id - self._entity = device dict_id = id(self) @@ -932,8 +889,6 @@ class ZWaveDeviceEntity(ZWaveBaseEntity): const.ATTR_VALUE_INDEX: self.values.primary.index, const.ATTR_VALUE_INSTANCE: self.values.primary.instance, const.ATTR_VALUE_ID: str(self.values.primary.value_id), - 'old_entity_id': self.old_entity_id, - 'new_entity_id': self.new_entity_id, } if self.power_consumption is not None: diff --git a/homeassistant/components/zwave/const.py b/homeassistant/components/zwave/const.py index e2524aefadf..bb4b33300e5 100644 --- a/homeassistant/components/zwave/const.py +++ b/homeassistant/components/zwave/const.py @@ -7,7 +7,6 @@ ATTR_ASSOCIATION = "association" ATTR_INSTANCE = "instance" ATTR_GROUP = "group" ATTR_VALUE_ID = "value_id" -ATTR_OBJECT_ID = "object_id" ATTR_MESSAGES = "messages" ATTR_NAME = "name" ATTR_RETURN_ROUTES = "return_routes" diff --git a/homeassistant/components/zwave/node_entity.py b/homeassistant/components/zwave/node_entity.py index de8ca0c1ab9..5a4b1b02504 100644 --- a/homeassistant/components/zwave/node_entity.py +++ b/homeassistant/components/zwave/node_entity.py @@ -4,11 +4,10 @@ import logging from homeassistant.core import callback from homeassistant.const import ATTR_BATTERY_LEVEL, ATTR_WAKEUP, ATTR_ENTITY_ID from homeassistant.helpers.entity import Entity -from homeassistant.util import slugify from .const import ( ATTR_NODE_ID, COMMAND_CLASS_WAKE_UP, ATTR_SCENE_ID, ATTR_SCENE_DATA, - ATTR_BASIC_LEVEL, EVENT_NODE_EVENT, EVENT_SCENE_ACTIVATED, DOMAIN, + ATTR_BASIC_LEVEL, EVENT_NODE_EVENT, EVENT_SCENE_ACTIVATED, COMMAND_CLASS_CENTRAL_SCENE) from .util import node_name @@ -41,8 +40,6 @@ class ZWaveBaseEntity(Entity): def __init__(self): """Initialize the base Z-Wave class.""" self._update_scheduled = False - self.old_entity_id = None - self.new_entity_id = None def maybe_schedule_update(self): """Maybe schedule state update. @@ -72,7 +69,7 @@ class ZWaveBaseEntity(Entity): class ZWaveNodeEntity(ZWaveBaseEntity): """Representation of a Z-Wave node.""" - def __init__(self, node, network, new_entity_ids): + def __init__(self, node, network): """Initialize node.""" # pylint: disable=import-error super().__init__() @@ -84,11 +81,6 @@ class ZWaveNodeEntity(ZWaveBaseEntity): self._name = node_name(self.node) self._product_name = node.product_name self._manufacturer_name = node.manufacturer_name - self.old_entity_id = "{}.{}_{}".format( - DOMAIN, slugify(self._name), self.node_id) - self.new_entity_id = "{}.{}".format(DOMAIN, slugify(self._name)) - if not new_entity_ids: - self.entity_id = self.old_entity_id self._attributes = {} self.wakeup_interval = None self.location = None @@ -229,8 +221,6 @@ class ZWaveNodeEntity(ZWaveBaseEntity): ATTR_NODE_NAME: self._name, ATTR_MANUFACTURER_NAME: self._manufacturer_name, ATTR_PRODUCT_NAME: self._product_name, - 'old_entity_id': self.old_entity_id, - 'new_entity_id': self.new_entity_id, } attrs.update(self._attributes) if self.battery_level is not None: diff --git a/tests/components/zwave/test_init.py b/tests/components/zwave/test_init.py index 828385b9ded..38feb3c687b 100644 --- a/tests/components/zwave/test_init.py +++ b/tests/components/zwave/test_init.py @@ -213,9 +213,7 @@ def test_node_discovery(hass, mock_openzwave): mock_receivers.append(receiver) with patch('pydispatch.dispatcher.connect', new=mock_connect): - yield from async_setup_component(hass, 'zwave', {'zwave': { - 'new_entity_ids': True, - }}) + yield from async_setup_component(hass, 'zwave', {'zwave': {}}) assert len(mock_receivers) == 1 @@ -237,7 +235,6 @@ def test_node_ignored(hass, mock_openzwave): with patch('pydispatch.dispatcher.connect', new=mock_connect): yield from async_setup_component(hass, 'zwave', {'zwave': { - 'new_entity_ids': True, 'device_config': { 'zwave.mock_node': { 'ignored': True, @@ -262,9 +259,7 @@ def test_value_discovery(hass, mock_openzwave): mock_receivers.append(receiver) with patch('pydispatch.dispatcher.connect', new=mock_connect): - yield from async_setup_component(hass, 'zwave', {'zwave': { - 'new_entity_ids': True, - }}) + yield from async_setup_component(hass, 'zwave', {'zwave': {}}) assert len(mock_receivers) == 1 @@ -289,9 +284,7 @@ def test_value_discovery_existing_entity(hass, mock_openzwave): mock_receivers.append(receiver) with patch('pydispatch.dispatcher.connect', new=mock_connect): - yield from async_setup_component(hass, 'zwave', {'zwave': { - 'new_entity_ids': True, - }}) + yield from async_setup_component(hass, 'zwave', {'zwave': {}}) assert len(mock_receivers) == 1 @@ -336,9 +329,7 @@ def test_power_schemes(hass, mock_openzwave): mock_receivers.append(receiver) with patch('pydispatch.dispatcher.connect', new=mock_connect): - yield from async_setup_component(hass, 'zwave', {'zwave': { - 'new_entity_ids': True, - }}) + yield from async_setup_component(hass, 'zwave', {'zwave': {}}) assert len(mock_receivers) == 1 @@ -380,9 +371,7 @@ def test_network_ready(hass, mock_openzwave): mock_receivers.append(receiver) with patch('pydispatch.dispatcher.connect', new=mock_connect): - yield from async_setup_component(hass, 'zwave', {'zwave': { - 'new_entity_ids': True, - }}) + yield from async_setup_component(hass, 'zwave', {'zwave': {}}) assert len(mock_receivers) == 1 @@ -409,9 +398,7 @@ def test_network_complete(hass, mock_openzwave): mock_receivers.append(receiver) with patch('pydispatch.dispatcher.connect', new=mock_connect): - yield from async_setup_component(hass, 'zwave', {'zwave': { - 'new_entity_ids': True, - }}) + yield from async_setup_component(hass, 'zwave', {'zwave': {}}) assert len(mock_receivers) == 1 @@ -441,9 +428,7 @@ class TestZWaveDeviceEntityValues(unittest.TestCase): self.hass = get_test_home_assistant() self.hass.start() - setup_component(self.hass, 'zwave', {'zwave': { - 'new_entity_ids': True, - }}) + setup_component(self.hass, 'zwave', {'zwave': {}}) self.hass.block_till_done() self.node = MockNode() @@ -472,9 +457,7 @@ class TestZWaveDeviceEntityValues(unittest.TestCase): command_class='mock_bad_class', node=self.node) self.entity_id = 'mock_component.mock_node_mock_value' - self.zwave_config = {'zwave': { - 'new_entity_ids': True, - }} + self.zwave_config = {'zwave': {}} self.device_config = {self.entity_id: {}} def tearDown(self): # pylint: disable=invalid-name @@ -781,9 +764,7 @@ class TestZWaveServices(unittest.TestCase): self.hass.start() # Initialize zwave - setup_component(self.hass, 'zwave', {'zwave': { - 'new_entity_ids': True, - }}) + setup_component(self.hass, 'zwave', {'zwave': {}}) self.hass.block_till_done() self.zwave_network = self.hass.data[DATA_NETWORK] self.zwave_network.state = MockNetwork.STATE_READY diff --git a/tests/components/zwave/test_node_entity.py b/tests/components/zwave/test_node_entity.py index e4afca31740..299821d3685 100644 --- a/tests/components/zwave/test_node_entity.py +++ b/tests/components/zwave/test_node_entity.py @@ -43,7 +43,7 @@ def test_node_event_activated(hass, mock_openzwave): node = mock_zwave.MockNode(node_id=11) with patch('pydispatch.dispatcher.connect', new=mock_connect): - entity = node_entity.ZWaveNodeEntity(node, mock_openzwave, True) + entity = node_entity.ZWaveNodeEntity(node, mock_openzwave) assert len(mock_receivers) == 1 @@ -86,7 +86,7 @@ def test_scene_activated(hass, mock_openzwave): node = mock_zwave.MockNode(node_id=11) with patch('pydispatch.dispatcher.connect', new=mock_connect): - entity = node_entity.ZWaveNodeEntity(node, mock_openzwave, True) + entity = node_entity.ZWaveNodeEntity(node, mock_openzwave) assert len(mock_receivers) == 1 @@ -129,7 +129,7 @@ def test_central_scene_activated(hass, mock_openzwave): node = mock_zwave.MockNode(node_id=11) with patch('pydispatch.dispatcher.connect', new=mock_connect): - entity = node_entity.ZWaveNodeEntity(node, mock_openzwave, True) + entity = node_entity.ZWaveNodeEntity(node, mock_openzwave) assert len(mock_receivers) == 1 @@ -185,7 +185,7 @@ class TestZWaveNodeEntity(unittest.TestCase): self.node.manufacturer_name = 'Test Manufacturer' self.node.product_name = 'Test Product' self.entity = node_entity.ZWaveNodeEntity(self.node, - self.zwave_network, True) + self.zwave_network) def test_network_node_changed_from_value(self): """Test for network_node_changed.""" @@ -226,8 +226,6 @@ class TestZWaveNodeEntity(unittest.TestCase): {'node_id': self.node.node_id, 'node_name': 'Mock Node', 'manufacturer_name': 'Test Manufacturer', - 'old_entity_id': 'zwave.mock_node_567', - 'new_entity_id': 'zwave.mock_node', 'product_name': 'Test Product'}, self.entity.device_state_attributes) @@ -286,8 +284,6 @@ class TestZWaveNodeEntity(unittest.TestCase): {'node_id': self.node.node_id, 'node_name': 'Mock Node', 'manufacturer_name': 'Test Manufacturer', - 'old_entity_id': 'zwave.mock_node_567', - 'new_entity_id': 'zwave.mock_node', 'product_name': 'Test Product', 'query_stage': 'Dynamic', 'is_awake': True, From 3dfc49d311be07d41b6134c755df64705629b806 Mon Sep 17 00:00:00 2001 From: Andrey Date: Sun, 11 Mar 2018 18:19:25 +0200 Subject: [PATCH 035/220] Make Sensibo climate registry_entity compliant (#13086) --- homeassistant/components/climate/sensibo.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/climate/sensibo.py b/homeassistant/components/climate/sensibo.py index 68b5eee35ef..e2a455aefc7 100644 --- a/homeassistant/components/climate/sensibo.py +++ b/homeassistant/components/climate/sensibo.py @@ -248,6 +248,11 @@ class SensiboClimate(ClimateDevice): return self._temperatures_list[-1] \ if self._temperatures_list else super().max_temp + @property + def unique_id(self): + """Return unique ID based on Sensibo ID.""" + return self._id + @asyncio.coroutine def async_set_temperature(self, **kwargs): """Set new target temperature.""" From d42b5a93ddf12466a0845c8d019440c9c5924033 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 11 Mar 2018 09:49:28 -0700 Subject: [PATCH 036/220] Implement Hue available property (#12939) --- homeassistant/components/light/hue.py | 11 ++++--- tests/components/light/test_hue.py | 42 +++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/light/hue.py b/homeassistant/components/light/hue.py index bee6840f346..75825683928 100644 --- a/homeassistant/components/light/hue.py +++ b/homeassistant/components/light/hue.py @@ -261,10 +261,13 @@ class HueLight(Light): """Return true if device is on.""" if self.is_group: return self.info['state']['any_on'] - elif self.allow_unreachable: - return self.info['state']['on'] - return self.info['state']['reachable'] and \ - self.info['state']['on'] + return self.info['state']['on'] + + @property + def available(self): + """Return if light is available.""" + return (self.is_group or self.allow_unreachable or + self.info['state']['reachable']) @property def supported_features(self): diff --git a/tests/components/light/test_hue.py b/tests/components/light/test_hue.py index 5c28ea9988f..559467d5e9a 100644 --- a/tests/components/light/test_hue.py +++ b/tests/components/light/test_hue.py @@ -501,3 +501,45 @@ class TestHueLight(unittest.TestCase): light = self.buildLight(info={}, is_group=True) self.assertIsNone(light.unique_id) + + +def test_available(): + """Test available property.""" + light = hue_light.HueLight( + info={'state': {'reachable': False}}, + allow_unreachable=False, + is_group=False, + + light_id=None, + bridge=mock.Mock(), + update_lights_cb=None, + allow_in_emulated_hue=False, + ) + + assert light.available is False + + light = hue_light.HueLight( + info={'state': {'reachable': False}}, + allow_unreachable=True, + is_group=False, + + light_id=None, + bridge=mock.Mock(), + update_lights_cb=None, + allow_in_emulated_hue=False, + ) + + assert light.available is True + + light = hue_light.HueLight( + info={'state': {'reachable': False}}, + allow_unreachable=False, + is_group=True, + + light_id=None, + bridge=mock.Mock(), + update_lights_cb=None, + allow_in_emulated_hue=False, + ) + + assert light.available is True From f5cc40024d9cf4b3bb10a86494c61205eb1d8d45 Mon Sep 17 00:00:00 2001 From: Matthias Urlichs Date: Sun, 11 Mar 2018 18:01:12 +0100 Subject: [PATCH 037/220] Rename homeassistant.util.async to .async_ (#13088) "async" is (going to be) a reserved word. --- docs/source/api/util.rst | 4 ++-- homeassistant/__main__.py | 2 +- homeassistant/components/camera/generic.py | 2 +- homeassistant/components/camera/proxy.py | 2 +- homeassistant/components/configurator.py | 2 +- homeassistant/components/device_tracker/__init__.py | 2 +- homeassistant/components/group/__init__.py | 2 +- .../components/image_processing/microsoft_face_identify.py | 2 +- homeassistant/components/image_processing/openalpr_local.py | 2 +- homeassistant/components/mqtt/__init__.py | 2 +- homeassistant/components/zone.py | 2 +- homeassistant/core.py | 2 +- homeassistant/helpers/condition.py | 2 +- homeassistant/helpers/discovery.py | 2 +- homeassistant/helpers/dispatcher.py | 2 +- homeassistant/helpers/entity.py | 2 +- homeassistant/helpers/entity_platform.py | 2 +- homeassistant/helpers/event.py | 2 +- homeassistant/helpers/script.py | 2 +- homeassistant/helpers/service.py | 2 +- homeassistant/helpers/state.py | 2 +- homeassistant/helpers/template.py | 2 +- homeassistant/setup.py | 2 +- homeassistant/util/{async.py => async_.py} | 0 homeassistant/util/logging.py | 2 +- tests/common.py | 2 +- tests/components/binary_sensor/test_template.py | 2 +- tests/components/camera/test_init.py | 2 +- tests/components/climate/test_generic_thermostat.py | 2 +- tests/components/device_tracker/test_init.py | 2 +- tests/components/device_tracker/test_owntracks.py | 2 +- tests/components/device_tracker/test_upc_connect.py | 2 +- tests/components/emulated_hue/test_upnp.py | 2 +- tests/components/media_player/test_async_helpers.py | 2 +- tests/components/media_player/test_universal.py | 2 +- tests/components/notify/test_group.py | 2 +- tests/components/sensor/test_uptime.py | 2 +- tests/components/switch/test_rest.py | 2 +- tests/components/test_init.py | 2 +- tests/helpers/test_aiohttp_client.py | 2 +- tests/helpers/test_state.py | 2 +- tests/test_config.py | 2 +- tests/test_core.py | 2 +- tests/util/test_async.py | 2 +- 44 files changed, 44 insertions(+), 44 deletions(-) rename homeassistant/util/{async.py => async_.py} (100%) diff --git a/docs/source/api/util.rst b/docs/source/api/util.rst index e31a1c98129..fb61cd94fe6 100644 --- a/docs/source/api/util.rst +++ b/docs/source/api/util.rst @@ -4,10 +4,10 @@ homeassistant.util package Submodules ---------- -homeassistant.util.async module +homeassistant.util.async_ module ------------------------------- -.. automodule:: homeassistant.util.async +.. automodule:: homeassistant.util.async_ :members: :undoc-members: :show-inheritance: diff --git a/homeassistant/__main__.py b/homeassistant/__main__.py index 319d00e6de5..3f6e0ef220f 100644 --- a/homeassistant/__main__.py +++ b/homeassistant/__main__.py @@ -272,7 +272,7 @@ def setup_and_run_hass(config_dir: str, if args.open_ui: # Imported here to avoid importing asyncio before monkey patch - from homeassistant.util.async import run_callback_threadsafe + from homeassistant.util.async_ import run_callback_threadsafe def open_browser(event): """Open the webinterface in a browser.""" diff --git a/homeassistant/components/camera/generic.py b/homeassistant/components/camera/generic.py index 3f8c4bedc75..2f5d8d28979 100644 --- a/homeassistant/components/camera/generic.py +++ b/homeassistant/components/camera/generic.py @@ -21,7 +21,7 @@ from homeassistant.components.camera import ( PLATFORM_SCHEMA, DEFAULT_CONTENT_TYPE, Camera) from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers import config_validation as cv -from homeassistant.util.async import run_coroutine_threadsafe +from homeassistant.util.async_ import run_coroutine_threadsafe _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/camera/proxy.py b/homeassistant/components/camera/proxy.py index 56b9db5c0ec..d045235c3ad 100644 --- a/homeassistant/components/camera/proxy.py +++ b/homeassistant/components/camera/proxy.py @@ -11,7 +11,7 @@ import async_timeout import voluptuous as vol -from homeassistant.util.async import run_coroutine_threadsafe +from homeassistant.util.async_ import run_coroutine_threadsafe from homeassistant.helpers import config_validation as cv import homeassistant.util.dt as dt_util diff --git a/homeassistant/components/configurator.py b/homeassistant/components/configurator.py index eaba08f0e89..2c159633a9b 100644 --- a/homeassistant/components/configurator.py +++ b/homeassistant/components/configurator.py @@ -15,7 +15,7 @@ from homeassistant.const import EVENT_TIME_CHANGED, ATTR_FRIENDLY_NAME, \ ATTR_ENTITY_PICTURE from homeassistant.loader import bind_hass from homeassistant.helpers.entity import async_generate_entity_id -from homeassistant.util.async import run_callback_threadsafe +from homeassistant.util.async_ import run_callback_threadsafe _LOGGER = logging.getLogger(__name__) _KEY_INSTANCE = 'configurator' diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index 9fea2bc104d..682496335a0 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -28,7 +28,7 @@ from homeassistant.helpers.typing import GPSType, ConfigType, HomeAssistantType import homeassistant.helpers.config_validation as cv from homeassistant.loader import get_component import homeassistant.util as util -from homeassistant.util.async import run_coroutine_threadsafe +from homeassistant.util.async_ import run_coroutine_threadsafe import homeassistant.util.dt as dt_util from homeassistant.util.yaml import dump diff --git a/homeassistant/components/group/__init__.py b/homeassistant/components/group/__init__.py index 3ece434f3c1..67ad8066aff 100644 --- a/homeassistant/components/group/__init__.py +++ b/homeassistant/components/group/__init__.py @@ -21,7 +21,7 @@ from homeassistant.helpers.entity import Entity, async_generate_entity_id from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.event import async_track_state_change import homeassistant.helpers.config_validation as cv -from homeassistant.util.async import run_coroutine_threadsafe +from homeassistant.util.async_ import run_coroutine_threadsafe DOMAIN = 'group' diff --git a/homeassistant/components/image_processing/microsoft_face_identify.py b/homeassistant/components/image_processing/microsoft_face_identify.py index 258731326ee..51f1cd42f47 100644 --- a/homeassistant/components/image_processing/microsoft_face_identify.py +++ b/homeassistant/components/image_processing/microsoft_face_identify.py @@ -17,7 +17,7 @@ from homeassistant.components.image_processing import ( PLATFORM_SCHEMA, ImageProcessingEntity, CONF_CONFIDENCE, CONF_SOURCE, CONF_ENTITY_ID, CONF_NAME, ATTR_ENTITY_ID, ATTR_CONFIDENCE) import homeassistant.helpers.config_validation as cv -from homeassistant.util.async import run_callback_threadsafe +from homeassistant.util.async_ import run_callback_threadsafe DEPENDENCIES = ['microsoft_face'] diff --git a/homeassistant/components/image_processing/openalpr_local.py b/homeassistant/components/image_processing/openalpr_local.py index ce06d98bf13..227e3269628 100644 --- a/homeassistant/components/image_processing/openalpr_local.py +++ b/homeassistant/components/image_processing/openalpr_local.py @@ -17,7 +17,7 @@ from homeassistant.const import STATE_UNKNOWN, CONF_REGION from homeassistant.components.image_processing import ( PLATFORM_SCHEMA, ImageProcessingEntity, CONF_CONFIDENCE, CONF_SOURCE, CONF_ENTITY_ID, CONF_NAME, ATTR_ENTITY_ID, ATTR_CONFIDENCE) -from homeassistant.util.async import run_callback_threadsafe +from homeassistant.util.async_ import run_callback_threadsafe _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 27e7c0358ad..590e16747c5 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -27,7 +27,7 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.loader import bind_hass from homeassistant.helpers import template, config_validation as cv from homeassistant.helpers.entity import Entity -from homeassistant.util.async import ( +from homeassistant.util.async_ import ( run_coroutine_threadsafe, run_callback_threadsafe) from homeassistant.const import ( EVENT_HOMEASSISTANT_STOP, CONF_VALUE_TEMPLATE, CONF_USERNAME, diff --git a/homeassistant/components/zone.py b/homeassistant/components/zone.py index 9ba503e6666..b1a94f3809c 100644 --- a/homeassistant/components/zone.py +++ b/homeassistant/components/zone.py @@ -16,7 +16,7 @@ from homeassistant.const import ( from homeassistant.loader import bind_hass from homeassistant.helpers import config_per_platform from homeassistant.helpers.entity import Entity, async_generate_entity_id -from homeassistant.util.async import run_callback_threadsafe +from homeassistant.util.async_ import run_callback_threadsafe from homeassistant.util.location import distance _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/core.py b/homeassistant/core.py index 543aba2a0e7..a486ee1adbf 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -33,7 +33,7 @@ from homeassistant.const import ( from homeassistant import loader from homeassistant.exceptions import ( HomeAssistantError, InvalidEntityFormatError, InvalidStateError) -from homeassistant.util.async import ( +from homeassistant.util.async_ import ( run_coroutine_threadsafe, run_callback_threadsafe, fire_coroutine_threadsafe) import homeassistant.util as util diff --git a/homeassistant/helpers/condition.py b/homeassistant/helpers/condition.py index bad6bfe83c3..f8f841cc449 100644 --- a/homeassistant/helpers/condition.py +++ b/homeassistant/helpers/condition.py @@ -18,7 +18,7 @@ from homeassistant.exceptions import TemplateError, HomeAssistantError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.sun import get_astral_event_date import homeassistant.util.dt as dt_util -from homeassistant.util.async import run_callback_threadsafe +from homeassistant.util.async_ import run_callback_threadsafe FROM_CONFIG_FORMAT = '{}_from_config' ASYNC_FROM_CONFIG_FORMAT = 'async_{}_from_config' diff --git a/homeassistant/helpers/discovery.py b/homeassistant/helpers/discovery.py index 6a527021c77..82322fec1e5 100644 --- a/homeassistant/helpers/discovery.py +++ b/homeassistant/helpers/discovery.py @@ -13,7 +13,7 @@ from homeassistant.const import ( ATTR_DISCOVERED, ATTR_SERVICE, EVENT_PLATFORM_DISCOVERED) from homeassistant.exceptions import HomeAssistantError from homeassistant.loader import DEPENDENCY_BLACKLIST -from homeassistant.util.async import run_callback_threadsafe +from homeassistant.util.async_ import run_callback_threadsafe EVENT_LOAD_PLATFORM = 'load_platform.{}' ATTR_PLATFORM = 'platform' diff --git a/homeassistant/helpers/dispatcher.py b/homeassistant/helpers/dispatcher.py index 8c41505bd29..136f4caa35a 100644 --- a/homeassistant/helpers/dispatcher.py +++ b/homeassistant/helpers/dispatcher.py @@ -3,7 +3,7 @@ import logging from homeassistant.core import callback from homeassistant.loader import bind_hass -from homeassistant.util.async import run_callback_threadsafe +from homeassistant.util.async_ import run_callback_threadsafe _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index f23a49c1851..4efe8d2f6c3 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -15,7 +15,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.config import DATA_CUSTOMIZE from homeassistant.exceptions import NoEntitySpecifiedError from homeassistant.util import ensure_unique_string, slugify -from homeassistant.util.async import run_callback_threadsafe +from homeassistant.util.async_ import run_callback_threadsafe _LOGGER = logging.getLogger(__name__) SLOW_UPDATE_WARNING = 10 diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index d28212a34d1..712121bbdb5 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -5,7 +5,7 @@ from datetime import timedelta from homeassistant.const import DEVICE_DEFAULT_NAME from homeassistant.core import callback, valid_entity_id, split_entity_id from homeassistant.exceptions import HomeAssistantError, PlatformNotReady -from homeassistant.util.async import ( +from homeassistant.util.async_ import ( run_callback_threadsafe, run_coroutine_threadsafe) import homeassistant.util.dt as dt_util diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index eab2d583f45..d69a556b0cc 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -8,7 +8,7 @@ from ..core import HomeAssistant, callback from ..const import ( ATTR_NOW, EVENT_STATE_CHANGED, EVENT_TIME_CHANGED, MATCH_ALL) from ..util import dt as dt_util -from ..util.async import run_callback_threadsafe +from ..util.async_ import run_callback_threadsafe # PyLint does not like the use of threaded_listener_factory # pylint: disable=invalid-name diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index 6530cb62485..abfdde8c8e1 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -16,7 +16,7 @@ from homeassistant.helpers.event import ( async_track_point_in_utc_time, async_track_template) from homeassistant.helpers.typing import ConfigType import homeassistant.util.dt as date_util -from homeassistant.util.async import ( +from homeassistant.util.async_ import ( run_coroutine_threadsafe, run_callback_threadsafe) _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index 7118cab211a..3595b258f12 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -13,7 +13,7 @@ from homeassistant.helpers import template from homeassistant.loader import get_component, bind_hass from homeassistant.util.yaml import load_yaml import homeassistant.helpers.config_validation as cv -from homeassistant.util.async import run_coroutine_threadsafe +from homeassistant.util.async_ import run_coroutine_threadsafe CONF_SERVICE = 'service' CONF_SERVICE_TEMPLATE = 'service_template' diff --git a/homeassistant/helpers/state.py b/homeassistant/helpers/state.py index 6be0dbae914..f97d7051459 100644 --- a/homeassistant/helpers/state.py +++ b/homeassistant/helpers/state.py @@ -40,7 +40,7 @@ from homeassistant.const import ( STATE_ON, STATE_OPEN, STATE_PAUSED, STATE_PLAYING, STATE_UNKNOWN, STATE_UNLOCKED, SERVICE_SELECT_OPTION) from homeassistant.core import State -from homeassistant.util.async import run_coroutine_threadsafe +from homeassistant.util.async_ import run_coroutine_threadsafe _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 6fab1c6c844..3dd65aa362c 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -20,7 +20,7 @@ from homeassistant.loader import bind_hass, get_component from homeassistant.util import convert from homeassistant.util import dt as dt_util from homeassistant.util import location as loc_util -from homeassistant.util.async import run_callback_threadsafe +from homeassistant.util.async_ import run_callback_threadsafe _LOGGER = logging.getLogger(__name__) _SENTINEL = object() diff --git a/homeassistant/setup.py b/homeassistant/setup.py index 5be1547242e..169a160af65 100644 --- a/homeassistant/setup.py +++ b/homeassistant/setup.py @@ -10,7 +10,7 @@ from homeassistant import requirements, core, loader, config as conf_util from homeassistant.config import async_notify_setup_error from homeassistant.const import EVENT_COMPONENT_LOADED, PLATFORM_FORMAT from homeassistant.exceptions import HomeAssistantError -from homeassistant.util.async import run_coroutine_threadsafe +from homeassistant.util.async_ import run_coroutine_threadsafe _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/util/async.py b/homeassistant/util/async_.py similarity index 100% rename from homeassistant/util/async.py rename to homeassistant/util/async_.py diff --git a/homeassistant/util/logging.py b/homeassistant/util/logging.py index 8a15c4f6320..f7306cae98b 100644 --- a/homeassistant/util/logging.py +++ b/homeassistant/util/logging.py @@ -3,7 +3,7 @@ import asyncio import logging import threading -from .async import run_coroutine_threadsafe +from .async_ import run_coroutine_threadsafe class HideSensitiveDataFilter(logging.Filter): diff --git a/tests/common.py b/tests/common.py index 15ce80a9552..bc84b3493a8 100644 --- a/tests/common.py +++ b/tests/common.py @@ -24,7 +24,7 @@ from homeassistant.const import ( EVENT_STATE_CHANGED, EVENT_PLATFORM_DISCOVERED, ATTR_SERVICE, ATTR_DISCOVERED, SERVER_PORT, EVENT_HOMEASSISTANT_CLOSE) from homeassistant.components import mqtt, recorder -from homeassistant.util.async import ( +from homeassistant.util.async_ import ( run_callback_threadsafe, run_coroutine_threadsafe) _TEST_INSTANCE_PORT = SERVER_PORT diff --git a/tests/components/binary_sensor/test_template.py b/tests/components/binary_sensor/test_template.py index c47f23bf902..18c095f4bc1 100644 --- a/tests/components/binary_sensor/test_template.py +++ b/tests/components/binary_sensor/test_template.py @@ -9,7 +9,7 @@ from homeassistant import setup from homeassistant.components.binary_sensor import template from homeassistant.exceptions import TemplateError from homeassistant.helpers import template as template_hlpr -from homeassistant.util.async import run_callback_threadsafe +from homeassistant.util.async_ import run_callback_threadsafe import homeassistant.util.dt as dt_util from tests.common import ( diff --git a/tests/components/camera/test_init.py b/tests/components/camera/test_init.py index 87612da9faa..465d6276ad5 100644 --- a/tests/components/camera/test_init.py +++ b/tests/components/camera/test_init.py @@ -9,7 +9,7 @@ from homeassistant.const import ATTR_ENTITY_PICTURE import homeassistant.components.camera as camera import homeassistant.components.http as http from homeassistant.exceptions import HomeAssistantError -from homeassistant.util.async import run_coroutine_threadsafe +from homeassistant.util.async_ import run_coroutine_threadsafe from tests.common import ( get_test_home_assistant, get_test_instance_port, assert_setup_component) diff --git a/tests/components/climate/test_generic_thermostat.py b/tests/components/climate/test_generic_thermostat.py index abc9e6d74c2..bd0b764c6fe 100644 --- a/tests/components/climate/test_generic_thermostat.py +++ b/tests/components/climate/test_generic_thermostat.py @@ -20,7 +20,7 @@ from homeassistant.const import ( ) from homeassistant import loader from homeassistant.util.unit_system import METRIC_SYSTEM -from homeassistant.util.async import run_coroutine_threadsafe +from homeassistant.util.async_ import run_coroutine_threadsafe from homeassistant.components import climate, input_boolean, switch import homeassistant.components as comps from tests.common import (assert_setup_component, get_test_home_assistant, diff --git a/tests/components/device_tracker/test_init.py b/tests/components/device_tracker/test_init.py index 9d122fa17b6..c051983d8fa 100644 --- a/tests/components/device_tracker/test_init.py +++ b/tests/components/device_tracker/test_init.py @@ -13,7 +13,7 @@ from homeassistant.core import callback, State from homeassistant.setup import setup_component, async_setup_component from homeassistant.helpers import discovery from homeassistant.loader import get_component -from homeassistant.util.async import run_coroutine_threadsafe +from homeassistant.util.async_ import run_coroutine_threadsafe import homeassistant.util.dt as dt_util from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_ENTITY_PICTURE, ATTR_FRIENDLY_NAME, ATTR_HIDDEN, diff --git a/tests/components/device_tracker/test_owntracks.py b/tests/components/device_tracker/test_owntracks.py index 2239e13e220..37a3e570b53 100644 --- a/tests/components/device_tracker/test_owntracks.py +++ b/tests/components/device_tracker/test_owntracks.py @@ -11,7 +11,7 @@ import homeassistant.components.device_tracker.owntracks as owntracks from homeassistant.setup import setup_component from homeassistant.components import device_tracker from homeassistant.const import CONF_PLATFORM, STATE_NOT_HOME -from homeassistant.util.async import run_coroutine_threadsafe +from homeassistant.util.async_ import run_coroutine_threadsafe USER = 'greg' DEVICE = 'phone' diff --git a/tests/components/device_tracker/test_upc_connect.py b/tests/components/device_tracker/test_upc_connect.py index 396d2b88b19..e45d70bc172 100644 --- a/tests/components/device_tracker/test_upc_connect.py +++ b/tests/components/device_tracker/test_upc_connect.py @@ -10,7 +10,7 @@ from homeassistant.const import ( CONF_PLATFORM, CONF_HOST) from homeassistant.components.device_tracker import DOMAIN import homeassistant.components.device_tracker.upc_connect as platform -from homeassistant.util.async import run_coroutine_threadsafe +from homeassistant.util.async_ import run_coroutine_threadsafe from tests.common import ( get_test_home_assistant, assert_setup_component, load_fixture, diff --git a/tests/components/emulated_hue/test_upnp.py b/tests/components/emulated_hue/test_upnp.py index b3032954431..555802f9a2c 100644 --- a/tests/components/emulated_hue/test_upnp.py +++ b/tests/components/emulated_hue/test_upnp.py @@ -9,7 +9,7 @@ from aiohttp.hdrs import CONTENT_TYPE from homeassistant import setup, const, core import homeassistant.components as core_components from homeassistant.components import emulated_hue, http -from homeassistant.util.async import run_coroutine_threadsafe +from homeassistant.util.async_ import run_coroutine_threadsafe from tests.common import get_test_instance_port, get_test_home_assistant diff --git a/tests/components/media_player/test_async_helpers.py b/tests/components/media_player/test_async_helpers.py index 6acbf5c2db3..11e324e9132 100644 --- a/tests/components/media_player/test_async_helpers.py +++ b/tests/components/media_player/test_async_helpers.py @@ -5,7 +5,7 @@ import asyncio import homeassistant.components.media_player as mp from homeassistant.const import ( STATE_PLAYING, STATE_PAUSED, STATE_ON, STATE_OFF, STATE_IDLE) -from homeassistant.util.async import run_coroutine_threadsafe +from homeassistant.util.async_ import run_coroutine_threadsafe from tests.common import get_test_home_assistant diff --git a/tests/components/media_player/test_universal.py b/tests/components/media_player/test_universal.py index b7f3165f11c..c9a1cdc79d8 100644 --- a/tests/components/media_player/test_universal.py +++ b/tests/components/media_player/test_universal.py @@ -11,7 +11,7 @@ import homeassistant.components.input_number as input_number import homeassistant.components.input_select as input_select import homeassistant.components.media_player as media_player import homeassistant.components.media_player.universal as universal -from homeassistant.util.async import run_coroutine_threadsafe +from homeassistant.util.async_ import run_coroutine_threadsafe from tests.common import mock_service, get_test_home_assistant diff --git a/tests/components/notify/test_group.py b/tests/components/notify/test_group.py index ed988b0f9b5..c96a49d7cb3 100644 --- a/tests/components/notify/test_group.py +++ b/tests/components/notify/test_group.py @@ -5,7 +5,7 @@ from unittest.mock import MagicMock, patch from homeassistant.setup import setup_component import homeassistant.components.notify as notify from homeassistant.components.notify import group, demo -from homeassistant.util.async import run_coroutine_threadsafe +from homeassistant.util.async_ import run_coroutine_threadsafe from tests.common import assert_setup_component, get_test_home_assistant diff --git a/tests/components/sensor/test_uptime.py b/tests/components/sensor/test_uptime.py index 541ea7ca771..a919e7d20db 100644 --- a/tests/components/sensor/test_uptime.py +++ b/tests/components/sensor/test_uptime.py @@ -3,7 +3,7 @@ import unittest from unittest.mock import patch from datetime import timedelta -from homeassistant.util.async import run_coroutine_threadsafe +from homeassistant.util.async_ import run_coroutine_threadsafe from homeassistant.setup import setup_component from homeassistant.components.sensor.uptime import UptimeSensor from tests.common import get_test_home_assistant diff --git a/tests/components/switch/test_rest.py b/tests/components/switch/test_rest.py index 064d0b1825b..e3f11ec19a0 100644 --- a/tests/components/switch/test_rest.py +++ b/tests/components/switch/test_rest.py @@ -5,7 +5,7 @@ import aiohttp import homeassistant.components.switch.rest as rest from homeassistant.setup import setup_component -from homeassistant.util.async import run_coroutine_threadsafe +from homeassistant.util.async_ import run_coroutine_threadsafe from homeassistant.helpers.template import Template from tests.common import get_test_home_assistant, assert_setup_component diff --git a/tests/components/test_init.py b/tests/components/test_init.py index eca4763b4b3..991982af9b2 100644 --- a/tests/components/test_init.py +++ b/tests/components/test_init.py @@ -14,7 +14,7 @@ import homeassistant.components as comps import homeassistant.helpers.intent as intent from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity -from homeassistant.util.async import run_coroutine_threadsafe +from homeassistant.util.async_ import run_coroutine_threadsafe from tests.common import ( get_test_home_assistant, mock_service, patch_yaml_files, mock_coro, diff --git a/tests/helpers/test_aiohttp_client.py b/tests/helpers/test_aiohttp_client.py index f5415ffe212..abe30d80a49 100644 --- a/tests/helpers/test_aiohttp_client.py +++ b/tests/helpers/test_aiohttp_client.py @@ -8,7 +8,7 @@ import pytest from homeassistant.core import EVENT_HOMEASSISTANT_CLOSE from homeassistant.setup import async_setup_component import homeassistant.helpers.aiohttp_client as client -from homeassistant.util.async import run_callback_threadsafe +from homeassistant.util.async_ import run_callback_threadsafe from tests.common import get_test_home_assistant diff --git a/tests/helpers/test_state.py b/tests/helpers/test_state.py index cc42bc8d7f8..f230d03e51e 100644 --- a/tests/helpers/test_state.py +++ b/tests/helpers/test_state.py @@ -7,7 +7,7 @@ from unittest.mock import patch import homeassistant.core as ha import homeassistant.components as core_components from homeassistant.const import (SERVICE_TURN_ON, SERVICE_TURN_OFF) -from homeassistant.util.async import run_coroutine_threadsafe +from homeassistant.util.async_ import run_coroutine_threadsafe from homeassistant.util import dt as dt_util from homeassistant.helpers import state from homeassistant.const import ( diff --git a/tests/test_config.py b/tests/test_config.py index 99c21493711..ab6b860ea8f 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -17,7 +17,7 @@ from homeassistant.const import ( CONF_UNIT_SYSTEM_METRIC, CONF_UNIT_SYSTEM_IMPERIAL, CONF_TEMPERATURE_UNIT) from homeassistant.util import location as location_util, dt as dt_util from homeassistant.util.yaml import SECRET_YAML -from homeassistant.util.async import run_coroutine_threadsafe +from homeassistant.util.async_ import run_coroutine_threadsafe from homeassistant.helpers.entity import Entity from homeassistant.components.config.group import ( CONFIG_PATH as GROUP_CONFIG_PATH) diff --git a/tests/test_core.py b/tests/test_core.py index 261b6385b04..7a1610c0966 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -14,7 +14,7 @@ import pytest import homeassistant.core as ha from homeassistant.exceptions import (InvalidEntityFormatError, InvalidStateError) -from homeassistant.util.async import run_coroutine_threadsafe +from homeassistant.util.async_ import run_coroutine_threadsafe import homeassistant.util.dt as dt_util from homeassistant.util.unit_system import (METRIC_SYSTEM) from homeassistant.const import ( diff --git a/tests/util/test_async.py b/tests/util/test_async.py index b6ae58a484f..3e57ea20b5c 100644 --- a/tests/util/test_async.py +++ b/tests/util/test_async.py @@ -5,7 +5,7 @@ from unittest import TestCase import pytest -from homeassistant.util import async as hasync +from homeassistant.util import async_ as hasync @patch('asyncio.coroutines.iscoroutine') From 26960283a02d8148f13b9bbf809a6d214d9f6343 Mon Sep 17 00:00:00 2001 From: Adam Mills Date: Sun, 11 Mar 2018 15:04:34 -0400 Subject: [PATCH 038/220] Config flow translations (#13066) * Development script for testing translation strings * Localize backend of config flow * Fix hue tests * Update hue.en.json * Move components to individual directories * Bridge -> bridge --- .../.translations/en.json | 24 +++++++++++++++++ .../__init__.py} | 6 +---- .../config_entry_example/strings.json | 24 +++++++++++++++++ .../components/hue/.translations/en.json | 26 +++++++++++++++++++ .../components/{hue.py => hue/__init__.py} | 15 +++++------ homeassistant/components/hue/strings.json | 26 +++++++++++++++++++ homeassistant/config_entries.py | 11 ++++---- script/translations_develop | 21 +++++++++++++++ .../components/config/test_config_entries.py | 17 +++++------- tests/components/test_hue.py | 6 ++--- tests/test_config_entries.py | 12 +++------ 11 files changed, 147 insertions(+), 41 deletions(-) create mode 100644 homeassistant/components/config_entry_example/.translations/en.json rename homeassistant/components/{config_entry_example.py => config_entry_example/__init__.py} (90%) create mode 100644 homeassistant/components/config_entry_example/strings.json create mode 100644 homeassistant/components/hue/.translations/en.json rename homeassistant/components/{hue.py => hue/__init__.py} (95%) create mode 100644 homeassistant/components/hue/strings.json create mode 100755 script/translations_develop diff --git a/homeassistant/components/config_entry_example/.translations/en.json b/homeassistant/components/config_entry_example/.translations/en.json new file mode 100644 index 00000000000..ec24d01ebc8 --- /dev/null +++ b/homeassistant/components/config_entry_example/.translations/en.json @@ -0,0 +1,24 @@ +{ + "config": { + "error": { + "invalid_object_id": "Invalid object ID" + }, + "step": { + "init": { + "data": { + "object_id": "Object ID" + }, + "description": "Please enter an object_id for the test entity.", + "title": "Pick object id" + }, + "name": { + "data": { + "name": "Name" + }, + "description": "Please enter a name for the test entity.", + "title": "Name of the entity" + } + }, + "title": "Config Entry Example" + } +} \ No newline at end of file diff --git a/homeassistant/components/config_entry_example.py b/homeassistant/components/config_entry_example/__init__.py similarity index 90% rename from homeassistant/components/config_entry_example.py rename to homeassistant/components/config_entry_example/__init__.py index 2d5ea728ff3..3ebfdc3a183 100644 --- a/homeassistant/components/config_entry_example.py +++ b/homeassistant/components/config_entry_example/__init__.py @@ -62,13 +62,11 @@ class ExampleConfigFlow(config_entries.ConfigFlowHandler): return (yield from self.async_step_name()) errors = { - 'object_id': 'Invalid object id.' + 'object_id': 'invalid_object_id' } return self.async_show_form( - title='Pick object id', step_id='init', - description="Please enter an object_id for the test entity.", data_schema=vol.Schema({ 'object_id': str }), @@ -92,9 +90,7 @@ class ExampleConfigFlow(config_entries.ConfigFlowHandler): ) return self.async_show_form( - title='Name of the entity', step_id='name', - description="Please enter a name for the test entity.", data_schema=vol.Schema({ 'name': str }), diff --git a/homeassistant/components/config_entry_example/strings.json b/homeassistant/components/config_entry_example/strings.json new file mode 100644 index 00000000000..a7a8cd4025b --- /dev/null +++ b/homeassistant/components/config_entry_example/strings.json @@ -0,0 +1,24 @@ +{ + "config": { + "title": "Config Entry Example", + "step": { + "init": { + "title": "Pick object id", + "description": "Please enter an object_id for the test entity.", + "data": { + "object_id": "Object ID" + } + }, + "name": { + "title": "Name of the entity", + "description": "Please enter a name for the test entity.", + "data": { + "name": "Name" + } + } + }, + "error": { + "invalid_object_id": "Invalid object ID" + } + } +} diff --git a/homeassistant/components/hue/.translations/en.json b/homeassistant/components/hue/.translations/en.json new file mode 100644 index 00000000000..ee2e01fdb17 --- /dev/null +++ b/homeassistant/components/hue/.translations/en.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "all_configured": "All Philips Hue bridges are already configured", + "discover_timeout": "Unable to discover Hue bridges", + "no_bridges": "No Philips Hue bridges discovered" + }, + "error": { + "linking": "Unknown linking error occurred.", + "register_failed": "Failed to register, please try again" + }, + "step": { + "init": { + "data": { + "host": "Host" + }, + "title": "Pick Hue bridge" + }, + "link": { + "description": "Press the button on the bridge to register Philips Hue with Home Assistant.\n\n![Location of button on bridge](/static/images/config_philips_hue.jpg)", + "title": "Link Hub" + } + }, + "title": "Philips Hue Bridge" + } +} diff --git a/homeassistant/components/hue.py b/homeassistant/components/hue/__init__.py similarity index 95% rename from homeassistant/components/hue.py rename to homeassistant/components/hue/__init__.py index d3870f0a3a1..92b23a74065 100644 --- a/homeassistant/components/hue.py +++ b/homeassistant/components/hue/__init__.py @@ -304,12 +304,12 @@ class HueFlowHandler(config_entries.ConfigFlowHandler): bridges = await discover_nupnp(websession=self._websession) except asyncio.TimeoutError: return self.async_abort( - reason='Unable to discover Hue bridges.' + reason='discover_timeout' ) if not bridges: return self.async_abort( - reason='No Philips Hue bridges discovered.' + reason='no_bridges' ) # Find already configured hosts @@ -322,7 +322,7 @@ class HueFlowHandler(config_entries.ConfigFlowHandler): if not hosts: return self.async_abort( - reason='All Philips Hue bridges are already configured.' + reason='all_configured' ) elif len(hosts) == 1: @@ -331,7 +331,6 @@ class HueFlowHandler(config_entries.ConfigFlowHandler): return self.async_show_form( step_id='init', - title='Pick Hue Bridge', data_schema=vol.Schema({ vol.Required('host'): vol.In(hosts) }) @@ -352,10 +351,10 @@ class HueFlowHandler(config_entries.ConfigFlowHandler): await bridge.initialize() except (asyncio.TimeoutError, aiohue.RequestError, aiohue.LinkButtonNotPressed): - errors['base'] = 'Failed to register, please try again.' + errors['base'] = 'register_failed' except aiohue.AiohueException: - errors['base'] = 'Unknown linking error occurred.' - _LOGGER.exception('Uknown Hue linking error occurred') + errors['base'] = 'linking' + _LOGGER.exception('Unknown Hue linking error occurred') else: return self.async_create_entry( title=bridge.config.name, @@ -368,8 +367,6 @@ class HueFlowHandler(config_entries.ConfigFlowHandler): return self.async_show_form( step_id='link', - title='Link Hub', - description=CONFIG_INSTRUCTIONS, errors=errors, ) diff --git a/homeassistant/components/hue/strings.json b/homeassistant/components/hue/strings.json new file mode 100644 index 00000000000..59b1ecd3cd1 --- /dev/null +++ b/homeassistant/components/hue/strings.json @@ -0,0 +1,26 @@ +{ + "config": { + "title": "Philips Hue Bridge", + "step": { + "init": { + "title": "Pick Hue bridge", + "data": { + "host": "Host" + } + }, + "link": { + "title": "Link Hub", + "description": "Press the button on the bridge to register Philips Hue with Home Assistant.\n\n![Location of button on bridge](/static/images/config_philips_hue.jpg)" + } + }, + "error": { + "register_failed": "Failed to register, please try again", + "linking": "Unknown linking error occurred." + }, + "abort": { + "discover_timeout": "Unable to discover Hue bridges", + "no_bridges": "No Philips Hue bridges discovered", + "all_configured": "All Philips Hue bridges are already configured" + } + } +} diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 230e48f0cec..eb05e800683 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -442,7 +442,7 @@ class FlowManager: 'Handler returned incorrect type: {}'.format(result['type'])) if result['type'] == RESULT_TYPE_FORM: - flow.cur_step = (result.pop('step_id'), result['data_schema']) + flow.cur_step = (result['step_id'], result['data_schema']) return result # Abort and Success results both finish the flow @@ -468,6 +468,7 @@ class ConfigFlowHandler: # Set by flow manager flow_id = None hass = None + domain = None source = SOURCE_USER cur_step = None @@ -475,15 +476,13 @@ class ConfigFlowHandler: # VERSION @callback - def async_show_form(self, *, title, step_id, description=None, - data_schema=None, errors=None): + def async_show_form(self, *, step_id, data_schema=None, errors=None): """Return the definition of a form to gather user input.""" return { 'type': RESULT_TYPE_FORM, 'flow_id': self.flow_id, - 'title': title, + 'domain': self.domain, 'step_id': step_id, - 'description': description, 'data_schema': data_schema, 'errors': errors, } @@ -494,6 +493,7 @@ class ConfigFlowHandler: return { 'type': RESULT_TYPE_CREATE_ENTRY, 'flow_id': self.flow_id, + 'domain': self.domain, 'title': title, 'data': data, } @@ -504,5 +504,6 @@ class ConfigFlowHandler: return { 'type': RESULT_TYPE_ABORT, 'flow_id': self.flow_id, + 'domain': self.domain, 'reason': reason } diff --git a/script/translations_develop b/script/translations_develop new file mode 100755 index 00000000000..eb9d685fa8e --- /dev/null +++ b/script/translations_develop @@ -0,0 +1,21 @@ +#!/usr/bin/env bash + +# Compile the current translation strings files for testing + +# Safe bash settings +# -e Exit on command fail +# -u Exit on unset variable +# -o pipefail Exit if piped command has error code +set -eu -o pipefail + +cd "$(dirname "$0")/.." + +mkdir -p build/translations-download + +script/translations_upload_merge.py + +# Use the generated translations upload file as the mock output from the +# Lokalise download +mv build/translations-upload.json build/translations-download/en.json + +script/translations_download_split.py diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index 1551ba74319..84667b8704b 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -101,9 +101,7 @@ def test_initialize_flow(hass, client): schema[vol.Required('password')] = str return self.async_show_form( - title='test-title', step_id='init', - description='test-description', data_schema=schema, errors={ 'username': 'Should be unique.' @@ -121,8 +119,8 @@ def test_initialize_flow(hass, client): assert data == { 'type': 'form', - 'title': 'test-title', - 'description': 'test-description', + 'domain': 'test', + 'step_id': 'init', 'data_schema': [ { 'name': 'username', @@ -157,6 +155,7 @@ def test_abort(hass, client): data = yield from resp.json() data.pop('flow_id') assert data == { + 'domain': 'test', 'reason': 'bla', 'type': 'abort' } @@ -186,6 +185,7 @@ def test_create_account(hass, client): data = yield from resp.json() data.pop('flow_id') assert data == { + 'domain': 'test', 'title': 'Test Entry', 'type': 'create_entry' } @@ -203,7 +203,6 @@ def test_two_step_flow(hass, client): @asyncio.coroutine def async_step_init(self, user_input=None): return self.async_show_form( - title='test-title', step_id='account', data_schema=vol.Schema({ 'user_title': str @@ -224,8 +223,8 @@ def test_two_step_flow(hass, client): flow_id = data.pop('flow_id') assert data == { 'type': 'form', - 'title': 'test-title', - 'description': None, + 'domain': 'test', + 'step_id': 'account', 'data_schema': [ { 'name': 'user_title', @@ -243,6 +242,7 @@ def test_two_step_flow(hass, client): data = yield from resp.json() data.pop('flow_id') assert data == { + 'domain': 'test', 'type': 'create_entry', 'title': 'user-title', } @@ -262,7 +262,6 @@ def test_get_progress_index(hass, client): def async_step_account(self, user_input=None): return self.async_show_form( step_id='account', - title='Finish setup' ) with patch.dict(HANDLERS, {'test': TestFlow}): @@ -292,9 +291,7 @@ def test_get_progress_flow(hass, client): schema[vol.Required('password')] = str return self.async_show_form( - title='test-title', step_id='init', - description='test-description', data_schema=schema, errors={ 'username': 'Should be unique.' diff --git a/tests/components/test_hue.py b/tests/components/test_hue.py index fa61cb2b69e..78f8b573666 100644 --- a/tests/components/test_hue.py +++ b/tests/components/test_hue.py @@ -552,7 +552,7 @@ async def test_flow_link_timeout(hass): assert result['type'] == 'form' assert result['step_id'] == 'link' assert result['errors'] == { - 'base': 'Failed to register, please try again.' + 'base': 'register_failed' } @@ -568,7 +568,7 @@ async def test_flow_link_button_not_pressed(hass): assert result['type'] == 'form' assert result['step_id'] == 'link' assert result['errors'] == { - 'base': 'Failed to register, please try again.' + 'base': 'register_failed' } @@ -584,5 +584,5 @@ async def test_flow_link_unknown_host(hass): assert result['type'] == 'form' assert result['step_id'] == 'link' assert result['errors'] == { - 'base': 'Failed to register, please try again.' + 'base': 'register_failed' } diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 3a1fe1d9d3e..5b1ec3b8ec0 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -226,14 +226,14 @@ def test_configure_reuses_handler_instance(manager): def async_step_init(self, user_input=None): self.handle_count += 1 return self.async_show_form( - title=str(self.handle_count), + errors={'base': str(self.handle_count)}, step_id='init') with patch.dict(config_entries.HANDLERS, {'test': TestFlow}): form = yield from manager.flow.async_init('test') - assert form['title'] == '1' + assert form['errors']['base'] == '1' form = yield from manager.flow.async_configure(form['flow_id']) - assert form['title'] == '2' + assert form['errors']['base'] == '2' assert len(manager.flow.async_progress()) == 1 assert len(manager.async_entries()) == 0 @@ -250,7 +250,6 @@ def test_configure_two_steps(manager): self.init_data = user_input return self.async_step_second() return self.async_show_form( - title='title', step_id='init', data_schema=vol.Schema([str]) ) @@ -263,7 +262,6 @@ def test_configure_two_steps(manager): data=self.init_data + user_input ) return self.async_show_form( - title='title', step_id='second', data_schema=vol.Schema([str]) ) @@ -299,9 +297,7 @@ def test_show_form(manager): @asyncio.coroutine def async_step_init(self, user_input=None): return self.async_show_form( - title='Hello form', step_id='init', - description='test-description', data_schema=schema, errors={ 'username': 'Should be unique.' @@ -311,8 +307,6 @@ def test_show_form(manager): with patch.dict(config_entries.HANDLERS, {'test': TestFlow}): form = yield from manager.flow.async_init('test') assert form['type'] == 'form' - assert form['title'] == 'Hello form' - assert form['description'] == 'test-description' assert form['data_schema'] is schema assert form['errors'] == { 'username': 'Should be unique.' From d0f089975dafc009ee7b6bb54abe0c67b509e7f7 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 11 Mar 2018 12:32:12 -0700 Subject: [PATCH 039/220] Run asyncio event loop in debug mode during tests (#13058) * Run asyncio event loop in debug mode during tests * Remove debug mode again --- homeassistant/components/camera/arlo.py | 5 ++--- homeassistant/components/climate/generic_thermostat.py | 2 +- tests/components/cover/test_template.py | 4 ++-- tests/components/zwave/test_init.py | 4 ++-- 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/camera/arlo.py b/homeassistant/components/camera/arlo.py index 4d461b0e0b5..f3e70c2bdd7 100644 --- a/homeassistant/components/camera/arlo.py +++ b/homeassistant/components/camera/arlo.py @@ -49,8 +49,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_devices, discovery_info=None): +def setup_platform(hass, config, add_devices, discovery_info=None): """Set up an Arlo IP Camera.""" arlo = hass.data.get(DATA_ARLO) if not arlo: @@ -60,7 +59,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): for camera in arlo.cameras: cameras.append(ArloCam(hass, camera, config)) - async_add_devices(cameras, True) + add_devices(cameras, True) class ArloCam(Camera): diff --git a/homeassistant/components/climate/generic_thermostat.py b/homeassistant/components/climate/generic_thermostat.py index b97dc221298..b5d3c3f7c25 100644 --- a/homeassistant/components/climate/generic_thermostat.py +++ b/homeassistant/components/climate/generic_thermostat.py @@ -229,7 +229,7 @@ class GenericThermostat(ClimateDevice): """List of available operation modes.""" return self._operation_list - def set_operation_mode(self, operation_mode): + async def async_set_operation_mode(self, operation_mode): """Set operation mode.""" if operation_mode == STATE_HEAT: self._current_operation = STATE_HEAT diff --git a/tests/components/cover/test_template.py b/tests/components/cover/test_template.py index 3d7aa3ce618..b786b463dad 100644 --- a/tests/components/cover/test_template.py +++ b/tests/components/cover/test_template.py @@ -135,7 +135,7 @@ class TestTemplateCover(unittest.TestCase): entity = self.hass.states.get('cover.test') attrs = dict() attrs['position'] = 42 - self.hass.states.async_set( + self.hass.states.set( entity.entity_id, entity.state, attributes=attrs) self.hass.block_till_done() @@ -148,7 +148,7 @@ class TestTemplateCover(unittest.TestCase): self.hass.block_till_done() entity = self.hass.states.get('cover.test') attrs['position'] = 0.0 - self.hass.states.async_set( + self.hass.states.set( entity.entity_id, entity.state, attributes=attrs) self.hass.block_till_done() diff --git a/tests/components/zwave/test_init.py b/tests/components/zwave/test_init.py index 38feb3c687b..cdbf91d09e5 100644 --- a/tests/components/zwave/test_init.py +++ b/tests/components/zwave/test_init.py @@ -302,7 +302,7 @@ def test_value_discovery_existing_entity(hass, mock_openzwave): 'current_temperature'] is None def mock_update(self): - self.hass.async_add_job(self.async_update_ha_state) + self.hass.add_job(self.async_update_ha_state) with patch.object(zwave.node_entity.ZWaveBaseEntity, 'maybe_schedule_update', new=mock_update): @@ -347,7 +347,7 @@ def test_power_schemes(hass, mock_openzwave): 'switch.mock_node_mock_value').attributes def mock_update(self): - self.hass.async_add_job(self.async_update_ha_state) + self.hass.add_job(self.async_update_ha_state) with patch.object(zwave.node_entity.ZWaveBaseEntity, 'maybe_schedule_update', new=mock_update): From 56b3cb05836af67395265bf78f69e755f27fceba Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 11 Mar 2018 12:33:07 -0700 Subject: [PATCH 040/220] Fix Tado doing async wrong (#13078) * Fix Tado doing async wrong * Remove last coroutine decorator --- .../components/device_tracker/tado.py | 28 ++++++++----------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/device_tracker/tado.py b/homeassistant/components/device_tracker/tado.py index dcf06036ea0..11d12322ff5 100644 --- a/homeassistant/components/device_tracker/tado.py +++ b/homeassistant/components/device_tracker/tado.py @@ -68,22 +68,18 @@ class TadoDeviceScanner(DeviceScanner): self.websession = async_create_clientsession( hass, cookie_jar=aiohttp.CookieJar(unsafe=True, loop=hass.loop)) - self.success_init = self._update_info() + self.success_init = asyncio.run_coroutine_threadsafe( + self._async_update_info(), hass.loop + ).result() + _LOGGER.info("Scanner initialized") - @asyncio.coroutine - def async_scan_devices(self): + async def async_scan_devices(self): """Scan for devices and return a list containing found device ids.""" - info = self._update_info() - - # Don't yield if we got None - if info is not None: - yield from info - + await self._async_update_info() return [device.mac for device in self.last_results] - @asyncio.coroutine - def async_get_device_name(self, device): + async def async_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] @@ -93,7 +89,7 @@ class TadoDeviceScanner(DeviceScanner): return None @Throttle(MIN_TIME_BETWEEN_SCANS) - def _update_info(self): + async def _async_update_info(self): """ Query Tado for device marked as at home. @@ -111,14 +107,14 @@ class TadoDeviceScanner(DeviceScanner): home_id=self.home_id, username=self.username, password=self.password) - response = yield from self.websession.get(url) + response = await self.websession.get(url) if response.status != 200: _LOGGER.warning( "Error %d on %s.", response.status, self.tadoapiurl) - return + return False - tado_json = yield from response.json() + tado_json = await response.json() except (asyncio.TimeoutError, aiohttp.ClientError): _LOGGER.error("Cannot load Tado data") @@ -139,7 +135,7 @@ class TadoDeviceScanner(DeviceScanner): self.last_results = last_results - _LOGGER.info( + _LOGGER.debug( "Tado presence query successful, %d device(s) at home", len(self.last_results) ) From dff4f6ce48c639b6332a94aecd1cdd5a54a1d91c Mon Sep 17 00:00:00 2001 From: Joe Lu Date: Sun, 11 Mar 2018 13:33:36 -0600 Subject: [PATCH 041/220] Integrated with py-synology:0.2.0 which has fix to auto-renew session when it's expired (#13079) --- homeassistant/components/camera/synology.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/camera/synology.py b/homeassistant/components/camera/synology.py index fca9cbbc7a5..8bbb3e8c632 100644 --- a/homeassistant/components/camera/synology.py +++ b/homeassistant/components/camera/synology.py @@ -20,7 +20,7 @@ from homeassistant.helpers.aiohttp_client import ( async_get_clientsession) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['py-synology==0.1.5'] +REQUIREMENTS = ['py-synology==0.2.0'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 07a967a181f..f7e6063ec7d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -632,7 +632,7 @@ py-cpuinfo==3.3.0 py-melissa-climate==1.0.6 # homeassistant.components.camera.synology -py-synology==0.1.5 +py-synology==0.2.0 # homeassistant.components.hdmi_cec pyCEC==0.4.13 From 1dc5fa145f8f4f5ff9887e2e53507e0b99bf027f Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Sun, 11 Mar 2018 20:42:58 +0100 Subject: [PATCH 042/220] Revert "Cast automatically drop connection (#12635)" (#13094) This reverts commit e14893416fb98d3ef9a37d816e0ee719105b33a1. --- homeassistant/components/media_player/cast.py | 9 +++------ tests/components/media_player/test_cast.py | 2 +- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/media_player/cast.py b/homeassistant/components/media_player/cast.py index dbcb53ec185..579f9b62864 100644 --- a/homeassistant/components/media_player/cast.py +++ b/homeassistant/components/media_player/cast.py @@ -7,7 +7,6 @@ https://home-assistant.io/components/media_player.cast/ # pylint: disable=import-error import logging import threading -import functools import voluptuous as vol @@ -35,7 +34,6 @@ CONF_IGNORE_CEC = 'ignore_cec' CAST_SPLASH = 'https://home-assistant.io/images/cast/splash.png' DEFAULT_PORT = 8009 -SOCKET_CLIENT_RETRIES = 10 SUPPORT_CAST = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_PREVIOUS_TRACK | \ @@ -78,7 +76,7 @@ def _setup_internal_discovery(hass: HomeAssistantType) -> None: try: # pylint: disable=protected-access chromecast = pychromecast._get_chromecast_from_host( - mdns, blocking=True, tries=SOCKET_CLIENT_RETRIES) + mdns, blocking=True) except pychromecast.ChromecastConnectionError: _LOGGER.debug("Can't set up cast with mDNS info %s. " "Assuming it's not a Chromecast", mdns) @@ -183,9 +181,8 @@ async def async_setup_platform(hass: HomeAssistantType, config: ConfigType, else: # Manually add a "normal" Chromecast, we can do that without discovery. try: - func = functools.partial(pychromecast.Chromecast, *want_host, - tries=SOCKET_CLIENT_RETRIES) - chromecast = await hass.async_add_job(func) + chromecast = await hass.async_add_job( + pychromecast.Chromecast, *want_host) except pychromecast.ChromecastConnectionError as err: _LOGGER.warning("Can't set up chromecast on %s: %s", want_host[0], err) diff --git a/tests/components/media_player/test_cast.py b/tests/components/media_player/test_cast.py index aaaad47d8dc..2075b4cf6e6 100644 --- a/tests/components/media_player/test_cast.py +++ b/tests/components/media_player/test_cast.py @@ -123,7 +123,7 @@ def test_internal_discovery_callback_only_generates_once(hass): return_value=chromecast) as gen_chromecast: discover_cast('the-service', chromecast) mdns = (chromecast.host, chromecast.port, chromecast.uuid, None, None) - gen_chromecast.assert_called_once_with(mdns, blocking=True, tries=10) + gen_chromecast.assert_called_once_with(mdns, blocking=True) discover_cast('the-service', chromecast) gen_chromecast.reset_mock() From 401e92f84e81c637355fa88557803d6f442fdba7 Mon Sep 17 00:00:00 2001 From: Greg Dowling Date: Sun, 11 Mar 2018 19:43:28 +0000 Subject: [PATCH 043/220] Bump pyvera to 0.2.42. Improve event loop robustness. (#13095) --- homeassistant/components/vera.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/vera.py b/homeassistant/components/vera.py index a7c10462e0d..5cc4de0d5ca 100644 --- a/homeassistant/components/vera.py +++ b/homeassistant/components/vera.py @@ -19,7 +19,7 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_STOP, CONF_LIGHTS, CONF_EXCLUDE) from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['pyvera==0.2.41'] +REQUIREMENTS = ['pyvera==0.2.42'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index f7e6063ec7d..949c6c8491e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1021,7 +1021,7 @@ pyunifi==2.13 # pyuserinput==0.1.11 # homeassistant.components.vera -pyvera==0.2.41 +pyvera==0.2.42 # homeassistant.components.media_player.vizio pyvizio==0.0.2 From 991c457430bac1fb98e92c940e162e39d8d6f214 Mon Sep 17 00:00:00 2001 From: tadly Date: Sun, 11 Mar 2018 20:46:16 +0100 Subject: [PATCH 044/220] Updated jsonrpc-websocket to 0.6 (#13096) Fix Kodi by updating jsonrpc-websocket to 0.6 --- homeassistant/components/media_player/kodi.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/media_player/kodi.py b/homeassistant/components/media_player/kodi.py index d14bf0fadaf..6450b2f5b35 100644 --- a/homeassistant/components/media_player/kodi.py +++ b/homeassistant/components/media_player/kodi.py @@ -31,7 +31,7 @@ from homeassistant.helpers import script, config_validation as cv from homeassistant.helpers.template import Template from homeassistant.util.yaml import dump -REQUIREMENTS = ['jsonrpc-async==0.6', 'jsonrpc-websocket==0.5'] +REQUIREMENTS = ['jsonrpc-async==0.6', 'jsonrpc-websocket==0.6'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 949c6c8491e..1f78970b3c3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -421,7 +421,7 @@ jsonpath==0.75 jsonrpc-async==0.6 # homeassistant.components.media_player.kodi -jsonrpc-websocket==0.5 +jsonrpc-websocket==0.6 # homeassistant.scripts.keyring keyring==11.0.0 From 14aa4e7694d7d6fe36084606fa2b2cbd428e5c66 Mon Sep 17 00:00:00 2001 From: Adam Mills Date: Sun, 11 Mar 2018 16:15:09 -0400 Subject: [PATCH 045/220] Lint script tweaks (#13093) * Also lint working tree files When performing a git diff of upstream/dev..., git is diffing against the current HEAD, but does not include working tree files. By manually calculating a merge-base SHA to diff against, git will still diff those files. * Don't pylint tests files, since we don't in CI * Use merge base for lazytox * Simplify files changed header --- script/lazytox.py | 4 +++- script/lint | 10 +++++----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/script/lazytox.py b/script/lazytox.py index 7c57b49c00d..2639d640753 100755 --- a/script/lazytox.py +++ b/script/lazytox.py @@ -94,7 +94,9 @@ async def git(): """Exec git.""" if len(sys.argv) > 2 and sys.argv[1] == '--': return sys.argv[2:] - _, log = await async_exec('git', 'diff', 'upstream/dev...', '--name-only') + _, log = await async_exec('git', 'merge-base', 'upstream/dev', 'HEAD') + merge_base = log.splitlines()[0] + _, log = await async_exec('git', 'diff', merge_base, '--name-only') return log.splitlines() diff --git a/script/lint b/script/lint index 44871dbc9a4..dc6884f4882 100755 --- a/script/lint +++ b/script/lint @@ -3,10 +3,10 @@ cd "$(dirname "$0")/.." -export files="`git diff upstream/dev... --diff-filter=d --name-only | grep -e '\.py$'`" -echo "=================================================" -echo "FILES CHANGED (git diff upstream/dev... --diff-filter=d --name-only)" -echo "=================================================" +export files="$(git diff $(git merge-base upstream/dev HEAD) --diff-filter=d --name-only | grep -e '\.py$')" +echo '=================================================' +echo '= FILES CHANGED =' +echo '=================================================' if [ -z "$files" ] ; then echo "No python file changed. Rather use: tox -e lint" exit @@ -19,5 +19,5 @@ flake8 --doctests $files echo "================" echo "LINT with pylint" echo "================" -pylint $files +pylint $(echo "$files" | grep -v '^tests.*') echo From 890197e4070a653e529b2d3991ff74c3ea80da6a Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Mon, 12 Mar 2018 19:42:08 +0100 Subject: [PATCH 046/220] asyncio.ensure_future Python 3.5 (#13141) --- homeassistant/components/spc.py | 7 +------ homeassistant/util/async_.py | 9 +-------- 2 files changed, 2 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/spc.py b/homeassistant/components/spc.py index 10544b3ef53..9742bc25c63 100644 --- a/homeassistant/components/spc.py +++ b/homeassistant/components/spc.py @@ -189,12 +189,7 @@ class SpcWebGateway: def start_listener(self, async_callback, *args): """Start the websocket listener.""" - try: - from asyncio import ensure_future - except ImportError: - from asyncio import async as ensure_future - - ensure_future(self._ws_listen(async_callback, *args)) + asyncio.ensure_future(self._ws_listen(async_callback, *args)) def _build_url(self, resource): return urljoin(self._api_url, "spc/{}".format(resource)) diff --git a/homeassistant/util/async_.py b/homeassistant/util/async_.py index ea8e5e3c874..5676a1d0844 100644 --- a/homeassistant/util/async_.py +++ b/homeassistant/util/async_.py @@ -5,14 +5,7 @@ import logging from asyncio import coroutines from asyncio.futures import Future -try: - # pylint: disable=ungrouped-imports - from asyncio import ensure_future -except ImportError: - # Python 3.4.3 and earlier has this as async - # pylint: disable=unused-import - from asyncio import async - ensure_future = async +from asyncio import ensure_future _LOGGER = logging.getLogger(__name__) From 02ad9c3574d4d557fadee76d393377f6e0f00af4 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Mon, 12 Mar 2018 20:26:51 +0100 Subject: [PATCH 047/220] Upgrade aiohttp to 3.0.7 (#13119) --- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- setup.py | 7 ++----- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 16b8815e5cf..44b6f04b992 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -5,7 +5,7 @@ pip>=8.0.3 jinja2>=2.10 voluptuous==0.11.1 typing>=3,<4 -aiohttp==3.0.6 +aiohttp==3.0.7 async_timeout==2.0.0 astral==1.5 certifi>=2017.4.17 diff --git a/requirements_all.txt b/requirements_all.txt index 1f78970b3c3..065b1851e10 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -6,7 +6,7 @@ pip>=8.0.3 jinja2>=2.10 voluptuous==0.11.1 typing>=3,<4 -aiohttp==3.0.6 +aiohttp==3.0.7 async_timeout==2.0.0 astral==1.5 certifi>=2017.4.17 diff --git a/setup.py b/setup.py index 024b2df3b38..9a1968d5444 100755 --- a/setup.py +++ b/setup.py @@ -4,7 +4,6 @@ from setuptools import setup, find_packages import homeassistant.const as hass_const - PROJECT_NAME = 'Home Assistant' PROJECT_PACKAGE_NAME = 'homeassistant' PROJECT_LICENSE = 'Apache License 2.0' @@ -50,16 +49,14 @@ REQUIRES = [ 'jinja2>=2.10', 'voluptuous==0.11.1', 'typing>=3,<4', - 'aiohttp==3.0.6', + 'aiohttp==3.0.7', 'async_timeout==2.0.0', 'astral==1.5', 'certifi>=2017.4.17', 'attrs==17.4.0', ] -MIN_PY_VERSION = '.'.join(map( - str, - hass_const.REQUIRED_PYTHON_VER)) +MIN_PY_VERSION = '.'.join(map(str, hass_const.REQUIRED_PYTHON_VER)) setup( name=PROJECT_PACKAGE_NAME, From 676c94561bcb387cd7e879e8c7c974a49224e338 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Mon, 12 Mar 2018 20:28:27 +0100 Subject: [PATCH 048/220] Upgrade astral to 1.6 (#13120) --- 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 44b6f04b992..c91d7c84aa9 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -7,7 +7,7 @@ voluptuous==0.11.1 typing>=3,<4 aiohttp==3.0.7 async_timeout==2.0.0 -astral==1.5 +astral==1.6 certifi>=2017.4.17 attrs==17.4.0 diff --git a/requirements_all.txt b/requirements_all.txt index 065b1851e10..cae4a78e6cb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -8,7 +8,7 @@ voluptuous==0.11.1 typing>=3,<4 aiohttp==3.0.7 async_timeout==2.0.0 -astral==1.5 +astral==1.6 certifi>=2017.4.17 attrs==17.4.0 diff --git a/setup.py b/setup.py index 9a1968d5444..2e44258c619 100755 --- a/setup.py +++ b/setup.py @@ -51,7 +51,7 @@ REQUIRES = [ 'typing>=3,<4', 'aiohttp==3.0.7', 'async_timeout==2.0.0', - 'astral==1.5', + 'astral==1.6', 'certifi>=2017.4.17', 'attrs==17.4.0', ] From 15d345c4ef66f309e079d5f73901c590a0f22a23 Mon Sep 17 00:00:00 2001 From: Federico Zivolo Date: Mon, 12 Mar 2018 20:33:04 +0100 Subject: [PATCH 049/220] fix: Support different JointSpace API versions (#13084) --- homeassistant/components/media_player/philips_js.py | 9 ++++++--- homeassistant/const.py | 1 + requirements_all.txt | 2 +- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/media_player/philips_js.py b/homeassistant/components/media_player/philips_js.py index 24981555007..29d336e4d7a 100644 --- a/homeassistant/components/media_player/philips_js.py +++ b/homeassistant/components/media_player/philips_js.py @@ -15,11 +15,11 @@ from homeassistant.components.media_player import ( SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_STEP, SUPPORT_PLAY, MediaPlayerDevice) from homeassistant.const import ( - CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON, STATE_UNKNOWN) + CONF_HOST, CONF_NAME, CONF_API_VERSION, STATE_OFF, STATE_ON, STATE_UNKNOWN) from homeassistant.helpers.script import Script from homeassistant.util import Throttle -REQUIREMENTS = ['ha-philipsjs==0.0.1'] +REQUIREMENTS = ['ha-philipsjs==0.0.2'] _LOGGER = logging.getLogger(__name__) @@ -36,10 +36,12 @@ CONF_ON_ACTION = 'turn_on_action' DEFAULT_DEVICE = 'default' DEFAULT_HOST = '127.0.0.1' DEFAULT_NAME = 'Philips TV' +DEFAULT_API_VERSION = '1' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_HOST, default=DEFAULT_HOST): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_API_VERSION, default=DEFAULT_API_VERSION): cv.string, vol.Optional(CONF_ON_ACTION): cv.SCRIPT_SCHEMA, }) @@ -51,9 +53,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None): name = config.get(CONF_NAME) host = config.get(CONF_HOST) + api_version = config.get(CONF_API_VERSION) turn_on_action = config.get(CONF_ON_ACTION) - tvapi = haphilipsjs.PhilipsTV(host) + tvapi = haphilipsjs.PhilipsTV(host, api_version) on_script = Script(hass, turn_on_action) if turn_on_action else None add_devices([PhilipsTV(tvapi, name, on_script)]) diff --git a/homeassistant/const.py b/homeassistant/const.py index 3c5373e5a9c..4ce2f503ad6 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -27,6 +27,7 @@ CONF_ADDRESS = 'address' CONF_AFTER = 'after' CONF_ALIAS = 'alias' CONF_API_KEY = 'api_key' +CONF_API_VERSION = 'api_version' CONF_AT = 'at' CONF_AUTHENTICATION = 'authentication' CONF_BASE = 'base' diff --git a/requirements_all.txt b/requirements_all.txt index cae4a78e6cb..bd4dbd8c61f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -332,7 +332,7 @@ gstreamer-player==1.1.0 ha-ffmpeg==1.9 # homeassistant.components.media_player.philips_js -ha-philipsjs==0.0.1 +ha-philipsjs==0.0.2 # homeassistant.components.sensor.geo_rss_events haversine==0.4.5 From 8d8b07abd5bc1f46b5353253e9f9b239251e614d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 12 Mar 2018 13:54:56 -0700 Subject: [PATCH 050/220] Fix unavailable property for wemo switch (#13106) * Fix unavailable property for wemo switch * Have subscriptions respect the lock * Move subscription callback to added to hass section --- homeassistant/components/switch/wemo.py | 91 +++++++++++++++++-------- 1 file changed, 62 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/switch/wemo.py b/homeassistant/components/switch/wemo.py index 4339c92bb60..4f06f941558 100644 --- a/homeassistant/components/switch/wemo.py +++ b/homeassistant/components/switch/wemo.py @@ -4,16 +4,19 @@ Support for WeMo switches. For more details about this component, please refer to the documentation at https://home-assistant.io/components/switch.wemo/ """ +import asyncio import logging from datetime import datetime, timedelta +import async_timeout + from homeassistant.components.switch import SwitchDevice from homeassistant.util import convert from homeassistant.const import ( STATE_OFF, STATE_ON, STATE_STANDBY, STATE_UNKNOWN) -from homeassistant.loader import get_component DEPENDENCIES = ['wemo'] +SCAN_INTERVAL = timedelta(seconds=10) _LOGGER = logging.getLogger(__name__) @@ -54,29 +57,35 @@ class WemoSwitch(SwitchDevice): self.maker_params = None self.coffeemaker_mode = None self._state = None + self._available = True + self._update_lock = None # look up model name once as it incurs network traffic self._model_name = self.wemo.model_name - wemo = get_component('wemo') - wemo.SUBSCRIPTION_REGISTRY.register(self.wemo) - wemo.SUBSCRIPTION_REGISTRY.on(self.wemo, None, self._update_callback) - - def _update_callback(self, _device, _type, _params): + def _subscription_callback(self, _device, _type, _params): """Update the state by the Wemo device.""" - _LOGGER.info("Subscription update for %s", _device) + _LOGGER.info("Subscription update for %s", self.name) updated = self.wemo.subscription_update(_type, _params) - self._update(force_update=(not updated)) + self.hass.add_job( + self._async_locked_subscription_callback(not updated)) - if not hasattr(self, 'hass'): + async def _async_locked_subscription_callback(self, force_update): + """Helper to handle an update from a subscription.""" + # If an update is in progress, we don't do anything + if self._update_lock.locked(): return - self.schedule_update_ha_state() + + await self._async_locked_update(force_update) + self.async_schedule_update_ha_state() @property def should_poll(self): - """No polling needed with subscriptions.""" - if self._model_name == 'Insight': - return True - return False + """Device should poll. + + Subscriptions push the state, however it won't detect if a device + is no longer available. Use polling to detect if a device is available. + """ + return True @property def unique_id(self): @@ -172,13 +181,7 @@ class WemoSwitch(SwitchDevice): @property def available(self): """Return true if switch is available.""" - if self._model_name == 'Insight' and self.insight_params is None: - return False - if self._model_name == 'Maker' and self.maker_params is None: - return False - if self._model_name == 'CoffeeMaker' and self.coffeemaker_mode is None: - return False - return True + return self._available @property def icon(self): @@ -189,21 +192,46 @@ class WemoSwitch(SwitchDevice): def turn_on(self, **kwargs): """Turn the switch on.""" - self._state = WEMO_ON self.wemo.on() - self.schedule_update_ha_state() def turn_off(self, **kwargs): """Turn the switch off.""" - self._state = WEMO_OFF self.wemo.off() - self.schedule_update_ha_state() - def update(self): - """Update WeMo state.""" - self._update(force_update=True) + async def async_added_to_hass(self): + """Wemo switch added to HASS.""" + # Define inside async context so we know our event loop + self._update_lock = asyncio.Lock() - def _update(self, force_update=True): + registry = self.hass.components.wemo.SUBSCRIPTION_REGISTRY + await self.hass.async_add_job(registry.register, self.wemo) + registry.on(self.wemo, None, self._subscription_callback) + + async def async_update(self): + """Update WeMo state. + + Wemo has an aggressive retry logic that sometimes can take over a + minute to return. If we don't get a state after 5 seconds, assume the + Wemo switch is unreachable. If update goes through, it will be made + available again. + """ + # If an update is in progress, we don't do anything + if self._update_lock.locked(): + return + + try: + with async_timeout.timeout(5): + await asyncio.shield(self._async_locked_update(True)) + except asyncio.TimeoutError: + _LOGGER.warning('Lost connection to %s', self.name) + self._available = False + + async def _async_locked_update(self, force_update): + """Try updating within an async lock.""" + async with self._update_lock: + await self.hass.async_add_job(self._update, force_update) + + def _update(self, force_update): """Update the device state.""" try: self._state = self.wemo.get_state(force_update) @@ -215,6 +243,11 @@ class WemoSwitch(SwitchDevice): self.maker_params = self.wemo.maker_params elif self._model_name == 'CoffeeMaker': self.coffeemaker_mode = self.wemo.mode + + if not self._available: + _LOGGER.info('Reconnected to %s', self.name) + self._available = True except AttributeError as err: _LOGGER.warning("Could not update status for %s (%s)", self.name, err) + self._available = False From 51b0cbefe3d8145b503e4b87311ce91ed9e09558 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 12 Mar 2018 13:55:22 -0700 Subject: [PATCH 051/220] Catch if bridge goes unavailable (#13109) --- homeassistant/components/hue/__init__.py | 1 + homeassistant/components/light/hue.py | 10 ++++++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/hue/__init__.py b/homeassistant/components/hue/__init__.py index 92b23a74065..f15052fbd67 100644 --- a/homeassistant/components/hue/__init__.py +++ b/homeassistant/components/hue/__init__.py @@ -181,6 +181,7 @@ class HueBridge(object): self.allow_in_emulated_hue = allow_in_emulated_hue self.allow_hue_groups = allow_hue_groups + self.available = True self.bridge = None self.lights = {} self.lightgroups = {} diff --git a/homeassistant/components/light/hue.py b/homeassistant/components/light/hue.py index 75825683928..661b7c2b3a1 100644 --- a/homeassistant/components/light/hue.py +++ b/homeassistant/components/light/hue.py @@ -123,15 +123,20 @@ def unthrottled_update_lights(hass, bridge, add_devices): api = bridge.get_api() except phue.PhueRequestTimeout: _LOGGER.warning("Timeout trying to reach the bridge") + bridge.available = False return except ConnectionRefusedError: _LOGGER.error("The bridge refused the connection") + bridge.available = False return except socket.error: # socket.error when we cannot reach Hue _LOGGER.exception("Cannot reach the bridge") + bridge.available = False return + bridge.available = True + new_lights = process_lights( hass, api, bridge, lambda **kw: update_lights(hass, bridge, add_devices, **kw)) @@ -266,8 +271,9 @@ class HueLight(Light): @property def available(self): """Return if light is available.""" - return (self.is_group or self.allow_unreachable or - self.info['state']['reachable']) + return self.bridge.available and (self.is_group or + self.allow_unreachable or + self.info['state']['reachable']) @property def supported_features(self): From 95a528a75fd396a322b867f576accefa9dd3508b Mon Sep 17 00:00:00 2001 From: Alok Saboo Date: Mon, 12 Mar 2018 16:56:33 -0400 Subject: [PATCH 052/220] Throttle Arlo api calls (#13143) --- homeassistant/components/arlo.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/arlo.py b/homeassistant/components/arlo.py index 7e51ec8c045..77201e5ead9 100644 --- a/homeassistant/components/arlo.py +++ b/homeassistant/components/arlo.py @@ -5,11 +5,13 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/arlo/ """ import logging +from datetime import timedelta import voluptuous as vol from requests.exceptions import HTTPError, ConnectTimeout from homeassistant.helpers import config_validation as cv +from homeassistant.util import Throttle from homeassistant.const import CONF_USERNAME, CONF_PASSWORD REQUIREMENTS = ['pyarlo==0.1.2'] @@ -45,6 +47,7 @@ def setup(hass, config): arlo = PyArlo(username, password, preload=False) if not arlo.is_connected: return False + arlo.update = Throttle(timedelta(seconds=10))(arlo.update) hass.data[DATA_ARLO] = arlo except (ConnectTimeout, HTTPError) as ex: _LOGGER.error("Unable to connect to Netgear Arlo: %s", str(ex)) From f9e07e617c6120e5a24f9928173c9b1c4b21c2ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Per=20Osb=C3=A4ck?= Date: Mon, 12 Mar 2018 21:57:13 +0100 Subject: [PATCH 053/220] update to async/await (#13137) --- homeassistant/components/light/zha.py | 42 ++++++++++------------ homeassistant/components/zha/__init__.py | 46 ++++++++++-------------- 2 files changed, 38 insertions(+), 50 deletions(-) diff --git a/homeassistant/components/light/zha.py b/homeassistant/components/light/zha.py index f50b3d7689b..7958fcabf13 100644 --- a/homeassistant/components/light/zha.py +++ b/homeassistant/components/light/zha.py @@ -4,7 +4,6 @@ Lights on Zigbee Home Automation networks. For more details on this platform, please refer to the documentation at https://home-assistant.io/components/light.zha/ """ -import asyncio import logging from homeassistant.components import light, zha @@ -23,8 +22,8 @@ CAPABILITIES_COLOR_TEMP = 0x10 UNSUPPORTED_ATTRIBUTE = 0x86 -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_devices, discovery_info=None): +async def async_setup_platform(hass, config, async_add_devices, + discovery_info=None): """Set up the Zigbee Home Automation lights.""" discovery_info = zha.get_discovery_info(hass, discovery_info) if discovery_info is None: @@ -32,7 +31,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): endpoint = discovery_info['endpoint'] if hasattr(endpoint, 'light_color'): - caps = yield from zha.safe_read( + caps = await zha.safe_read( endpoint.light_color, ['color_capabilities']) discovery_info['color_capabilities'] = caps.get('color_capabilities') if discovery_info['color_capabilities'] is None: @@ -40,7 +39,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): # attribute. In this version XY support is mandatory, but we need # to probe to determine if the device supports color temperature. discovery_info['color_capabilities'] = CAPABILITIES_COLOR_XY - result = yield from zha.safe_read( + result = await zha.safe_read( endpoint.light_color, ['color_temperature']) if result.get('color_temperature') is not UNSUPPORTED_ATTRIBUTE: discovery_info['color_capabilities'] |= CAPABILITIES_COLOR_TEMP @@ -83,14 +82,13 @@ class Light(zha.Entity, light.Light): return False return bool(self._state) - @asyncio.coroutine - def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): """Turn the entity on.""" duration = kwargs.get(light.ATTR_TRANSITION, DEFAULT_DURATION) duration = duration * 10 # tenths of s if light.ATTR_COLOR_TEMP in kwargs: temperature = kwargs[light.ATTR_COLOR_TEMP] - yield from self._endpoint.light_color.move_to_color_temp( + await self._endpoint.light_color.move_to_color_temp( temperature, duration) self._color_temp = temperature @@ -102,7 +100,7 @@ class Light(zha.Entity, light.Light): self._xy_color = (xyb[0], xyb[1]) self._brightness = xyb[2] if light.ATTR_XY_COLOR in kwargs or light.ATTR_RGB_COLOR in kwargs: - yield from self._endpoint.light_color.move_to_color( + await self._endpoint.light_color.move_to_color( int(self._xy_color[0] * 65535), int(self._xy_color[1] * 65535), duration, @@ -113,7 +111,7 @@ class Light(zha.Entity, light.Light): light.ATTR_BRIGHTNESS, self._brightness or 255) self._brightness = brightness # Move to level with on/off: - yield from self._endpoint.level.move_to_level_with_on_off( + await self._endpoint.level.move_to_level_with_on_off( brightness, duration ) @@ -121,14 +119,13 @@ class Light(zha.Entity, light.Light): self.async_schedule_update_ha_state() return - yield from self._endpoint.on_off.on() + await self._endpoint.on_off.on() self._state = 1 self.async_schedule_update_ha_state() - @asyncio.coroutine - def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs): """Turn the entity off.""" - yield from self._endpoint.on_off.off() + await self._endpoint.on_off.off() self._state = 0 self.async_schedule_update_ha_state() @@ -152,26 +149,25 @@ class Light(zha.Entity, light.Light): """Flag supported features.""" return self._supported_features - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Retrieve latest state.""" - result = yield from zha.safe_read(self._endpoint.on_off, ['on_off']) + result = await zha.safe_read(self._endpoint.on_off, ['on_off']) self._state = result.get('on_off', self._state) if self._supported_features & light.SUPPORT_BRIGHTNESS: - result = yield from zha.safe_read(self._endpoint.level, - ['current_level']) + result = await zha.safe_read(self._endpoint.level, + ['current_level']) self._brightness = result.get('current_level', self._brightness) if self._supported_features & light.SUPPORT_COLOR_TEMP: - result = yield from zha.safe_read(self._endpoint.light_color, - ['color_temperature']) + result = await zha.safe_read(self._endpoint.light_color, + ['color_temperature']) self._color_temp = result.get('color_temperature', self._color_temp) if self._supported_features & light.SUPPORT_XY_COLOR: - result = yield from zha.safe_read(self._endpoint.light_color, - ['current_x', 'current_y']) + result = await zha.safe_read(self._endpoint.light_color, + ['current_x', 'current_y']) if 'current_x' in result and 'current_y' in result: self._xy_color = (result['current_x'], result['current_y']) diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index 88ca29101ad..39419034545 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -4,7 +4,6 @@ Support for ZigBee Home Automation devices. For more details about this component, please refer to the documentation at https://home-assistant.io/components/zha/ """ -import asyncio import collections import enum import logging @@ -80,8 +79,7 @@ APPLICATION_CONTROLLER = None _LOGGER = logging.getLogger(__name__) -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Set up ZHA. Will automatically load components to support devices found on the network. @@ -100,35 +98,33 @@ def async_setup(hass, config): from zigpy_xbee.zigbee.application import ControllerApplication radio = zigpy_xbee.api.XBee() - yield from radio.connect(usb_path, baudrate) + await radio.connect(usb_path, baudrate) database = config[DOMAIN].get(CONF_DATABASE) APPLICATION_CONTROLLER = ControllerApplication(radio, database) listener = ApplicationListener(hass, config) APPLICATION_CONTROLLER.add_listener(listener) - yield from APPLICATION_CONTROLLER.startup(auto_form=True) + await APPLICATION_CONTROLLER.startup(auto_form=True) for device in APPLICATION_CONTROLLER.devices.values(): hass.async_add_job(listener.async_device_initialized(device, False)) - @asyncio.coroutine - def permit(service): + async def permit(service): """Allow devices to join this network.""" duration = service.data.get(ATTR_DURATION) _LOGGER.info("Permitting joins for %ss", duration) - yield from APPLICATION_CONTROLLER.permit(duration) + await APPLICATION_CONTROLLER.permit(duration) hass.services.async_register(DOMAIN, SERVICE_PERMIT, permit, schema=SERVICE_SCHEMAS[SERVICE_PERMIT]) - @asyncio.coroutine - def remove(service): + async def remove(service): """Remove a node from the network.""" from bellows.types import EmberEUI64, uint8_t ieee = service.data.get(ATTR_IEEE) ieee = EmberEUI64([uint8_t(p, base=16) for p in ieee.split(':')]) _LOGGER.info("Removing node %s", ieee) - yield from APPLICATION_CONTROLLER.remove(ieee) + await APPLICATION_CONTROLLER.remove(ieee) hass.services.async_register(DOMAIN, SERVICE_REMOVE, remove, schema=SERVICE_SCHEMAS[SERVICE_REMOVE]) @@ -168,8 +164,7 @@ class ApplicationListener: for device_entity in self._device_registry[device.ieee]: self._hass.async_add_job(device_entity.async_remove()) - @asyncio.coroutine - def async_device_initialized(self, device, join): + async def async_device_initialized(self, device, join): """Handle device joined and basic information discovered (async).""" import zigpy.profiles import homeassistant.components.zha.const as zha_const @@ -179,7 +174,7 @@ class ApplicationListener: if endpoint_id == 0: # ZDO continue - discovered_info = yield from _discover_endpoint_info(endpoint) + discovered_info = await _discover_endpoint_info(endpoint) component = None profile_clusters = ([], []) @@ -218,7 +213,7 @@ class ApplicationListener: discovery_info.update(discovered_info) self._hass.data[DISCOVERY_KEY][device_key] = discovery_info - yield from discovery.async_load_platform( + await discovery.async_load_platform( self._hass, component, DOMAIN, @@ -247,7 +242,7 @@ class ApplicationListener: discovery_info.update(discovered_info) self._hass.data[DISCOVERY_KEY][cluster_key] = discovery_info - yield from discovery.async_load_platform( + await discovery.async_load_platform( self._hass, component, DOMAIN, @@ -323,8 +318,7 @@ class Entity(entity.Entity): pass -@asyncio.coroutine -def _discover_endpoint_info(endpoint): +async def _discover_endpoint_info(endpoint): """Find some basic information about an endpoint.""" extra_info = { 'manufacturer': None, @@ -333,20 +327,19 @@ def _discover_endpoint_info(endpoint): if 0 not in endpoint.in_clusters: return extra_info - @asyncio.coroutine - def read(attributes): + async def read(attributes): """Read attributes and update extra_info convenience function.""" - result, _ = yield from endpoint.in_clusters[0].read_attributes( + result, _ = await endpoint.in_clusters[0].read_attributes( attributes, allow_cache=True, ) extra_info.update(result) - yield from read(['manufacturer', 'model']) + await read(['manufacturer', 'model']) if extra_info['manufacturer'] is None or extra_info['model'] is None: # Some devices fail at returning multiple results. Attempt separately. - yield from read(['manufacturer']) - yield from read(['model']) + await read(['manufacturer']) + await read(['model']) for key, value in extra_info.items(): if isinstance(value, bytes): @@ -376,8 +369,7 @@ def get_discovery_info(hass, discovery_info): return all_discovery_info.get(discovery_key, None) -@asyncio.coroutine -def safe_read(cluster, attributes): +async def safe_read(cluster, attributes): """Swallow all exceptions from network read. If we throw during initialization, setup fails. Rather have an entity that @@ -385,7 +377,7 @@ def safe_read(cluster, attributes): probably only be used during initialization. """ try: - result, _ = yield from cluster.read_attributes( + result, _ = await cluster.read_attributes( attributes, allow_cache=False, ) From 54e0cc130453833c7040bc442f69b4d5cc8e4080 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Mon, 12 Mar 2018 22:00:10 +0100 Subject: [PATCH 054/220] Upgrade mypy to 0.570 (#13128) --- requirements_test.txt | 18 +++++++++--------- requirements_test_all.txt | 18 +++++++++--------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/requirements_test.txt b/requirements_test.txt index d56a7085c74..34506f5c40e 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,17 +1,17 @@ # 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 -flake8==3.5 -pylint==1.8.2 -mypy==0.560 -pydocstyle==1.1.1 +asynctest>=0.11.1 coveralls==1.2.0 -pytest==3.3.1 +flake8-docstrings==1.0.3 +flake8==3.5 +mock-open==1.3.1 +mypy==0.570 +pydocstyle==1.1.1 +pylint==1.8.2 pytest-aiohttp==0.3.0 pytest-cov==2.5.1 -pytest-timeout>=1.2.1 pytest-sugar==0.9.0 +pytest-timeout>=1.2.1 +pytest==3.3.1 requests_mock==1.4 -mock-open==1.3.1 -flake8-docstrings==1.0.3 -asynctest>=0.11.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 460e70cbca5..2f4768fd90b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2,20 +2,20 @@ # 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 -flake8==3.5 -pylint==1.8.2 -mypy==0.560 -pydocstyle==1.1.1 +asynctest>=0.11.1 coveralls==1.2.0 -pytest==3.3.1 +flake8-docstrings==1.0.3 +flake8==3.5 +mock-open==1.3.1 +mypy==0.570 +pydocstyle==1.1.1 +pylint==1.8.2 pytest-aiohttp==0.3.0 pytest-cov==2.5.1 -pytest-timeout>=1.2.1 pytest-sugar==0.9.0 +pytest-timeout>=1.2.1 +pytest==3.3.1 requests_mock==1.4 -mock-open==1.3.1 -flake8-docstrings==1.0.3 -asynctest>=0.11.1 # homeassistant.components.homekit From bbbb44b999eb2bf6c20071da16a1f6f2b9d430b3 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Mon, 12 Mar 2018 22:01:05 +0100 Subject: [PATCH 055/220] Upgrade TwitterAPI to 2.4.10 (#13126) --- homeassistant/components/notify/twitter.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/notify/twitter.py b/homeassistant/components/notify/twitter.py index c6f4fa0dd5f..db7de8e40a0 100644 --- a/homeassistant/components/notify/twitter.py +++ b/homeassistant/components/notify/twitter.py @@ -19,7 +19,7 @@ from homeassistant.components.notify import ( from homeassistant.const import CONF_ACCESS_TOKEN, CONF_USERNAME from homeassistant.helpers.event import async_track_point_in_time -REQUIREMENTS = ['TwitterAPI==2.4.8'] +REQUIREMENTS = ['TwitterAPI==2.4.10'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index bd4dbd8c61f..1ade4ab1be9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -52,7 +52,7 @@ SoCo==0.14 TravisPy==0.3.5 # homeassistant.components.notify.twitter -TwitterAPI==2.4.8 +TwitterAPI==2.4.10 # homeassistant.components.notify.yessssms YesssSMS==0.1.1b3 From 1202134964f0341a1cab9d6405f56154d0107ccd Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Mon, 12 Mar 2018 22:01:32 +0100 Subject: [PATCH 056/220] Upgrade youtube_dl to 2018.03.10 (#13124) --- 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 265784be74d..e10a713995b 100644 --- a/homeassistant/components/media_extractor.py +++ b/homeassistant/components/media_extractor.py @@ -14,7 +14,7 @@ from homeassistant.components.media_player import ( SERVICE_PLAY_MEDIA) from homeassistant.helpers import config_validation as cv -REQUIREMENTS = ['youtube_dl==2018.02.11'] +REQUIREMENTS = ['youtube_dl==2018.03.10'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 1ade4ab1be9..2a4f5b1fa44 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1298,7 +1298,7 @@ yeelight==0.4.0 yeelightsunflower==0.0.8 # homeassistant.components.media_extractor -youtube_dl==2018.02.11 +youtube_dl==2018.03.10 # homeassistant.components.light.zengge zengge==0.2 From 6ab4a408d2b49a91cd72963d883635030e24aa98 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Mon, 12 Mar 2018 22:02:03 +0100 Subject: [PATCH 057/220] Upgrade zeroconf to 0.20.0 (#13123) --- homeassistant/components/zeroconf.py | 3 ++- requirements_all.txt | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zeroconf.py b/homeassistant/components/zeroconf.py index 17c2643ecc3..675c3a65e5f 100644 --- a/homeassistant/components/zeroconf.py +++ b/homeassistant/components/zeroconf.py @@ -12,12 +12,13 @@ import voluptuous as vol from homeassistant import util from homeassistant.const import (EVENT_HOMEASSISTANT_STOP, __version__) +REQUIREMENTS = ['zeroconf==0.20.0'] + _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['api'] DOMAIN = 'zeroconf' -REQUIREMENTS = ['zeroconf==0.19.1'] ZEROCONF_TYPE = '_home-assistant._tcp.local.' diff --git a/requirements_all.txt b/requirements_all.txt index 2a4f5b1fa44..41f6000c8fb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1304,7 +1304,7 @@ youtube_dl==2018.03.10 zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.19.1 +zeroconf==0.20.0 # homeassistant.components.media_player.ziggo_mediabox_xl ziggo-mediabox-xl==1.0.0 From c5330a13b6fbb0663c1f12202c9c45d0edd7855c Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Mon, 12 Mar 2018 22:02:36 +0100 Subject: [PATCH 058/220] Upgrade schiene to 0.22 (#13121) --- homeassistant/components/sensor/deutsche_bahn.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/deutsche_bahn.py b/homeassistant/components/sensor/deutsche_bahn.py index 2b125155892..ec9b14883a9 100644 --- a/homeassistant/components/sensor/deutsche_bahn.py +++ b/homeassistant/components/sensor/deutsche_bahn.py @@ -14,7 +14,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity import homeassistant.util.dt as dt_util -REQUIREMENTS = ['schiene==0.21'] +REQUIREMENTS = ['schiene==0.22'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 41f6000c8fb..d53f4e39b36 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1093,7 +1093,7 @@ samsungctl[websocket]==0.7.1 satel_integra==0.1.0 # homeassistant.components.sensor.deutsche_bahn -schiene==0.21 +schiene==0.22 # homeassistant.components.scsgate scsgate==0.1.0 From ae286a550b8c5e0652d3a33037c8df5586b22942 Mon Sep 17 00:00:00 2001 From: Jeroen ter Heerdt Date: Mon, 12 Mar 2018 22:03:05 +0100 Subject: [PATCH 059/220] Adding check for empty discovery info in alarm control panel Egardia. (#13114) --- homeassistant/components/alarm_control_panel/egardia.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/alarm_control_panel/egardia.py b/homeassistant/components/alarm_control_panel/egardia.py index 64e165f6b16..845eb81bbe0 100644 --- a/homeassistant/components/alarm_control_panel/egardia.py +++ b/homeassistant/components/alarm_control_panel/egardia.py @@ -33,6 +33,8 @@ STATES = { def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Egardia platform.""" + if discovery_info is None: + return device = EgardiaAlarm( discovery_info['name'], hass.data[EGARDIA_DEVICE], From 0a2e949e0a39b2f1b5622c90cda59084ff0269d2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 12 Mar 2018 15:22:08 -0700 Subject: [PATCH 060/220] Remove crazy JSON encoding things that are no longer used (#13029) Catch JSON encoding errors in HTTP view --- homeassistant/components/http/view.py | 10 +++++++--- homeassistant/components/websocket_api.py | 6 +++++- homeassistant/remote.py | 12 +----------- tests/components/http/test_view.py | 15 +++++++++++++++ 4 files changed, 28 insertions(+), 15 deletions(-) create mode 100644 tests/components/http/test_view.py diff --git a/homeassistant/components/http/view.py b/homeassistant/components/http/view.py index 299a10e9f5a..81c6ea4bcfb 100644 --- a/homeassistant/components/http/view.py +++ b/homeassistant/components/http/view.py @@ -9,7 +9,7 @@ import json import logging from aiohttp import web -from aiohttp.web_exceptions import HTTPUnauthorized +from aiohttp.web_exceptions import HTTPUnauthorized, HTTPInternalServerError import homeassistant.remote as rem from homeassistant.core import is_callback @@ -31,8 +31,12 @@ class HomeAssistantView(object): # pylint: disable=no-self-use def json(self, result, status_code=200, headers=None): """Return a JSON response.""" - msg = json.dumps( - result, sort_keys=True, cls=rem.JSONEncoder).encode('UTF-8') + try: + msg = json.dumps( + result, sort_keys=True, cls=rem.JSONEncoder).encode('UTF-8') + except TypeError as err: + _LOGGER.error('Unable to serialize to JSON: %s\n%s', err, result) + raise HTTPInternalServerError response = web.Response( body=msg, content_type=CONTENT_TYPE_JSON, status=status_code, headers=headers) diff --git a/homeassistant/components/websocket_api.py b/homeassistant/components/websocket_api.py index 47ef2c3eace..1e23ad19897 100644 --- a/homeassistant/components/websocket_api.py +++ b/homeassistant/components/websocket_api.py @@ -240,7 +240,11 @@ class ActiveConnection: if message is None: break self.debug("Sending", message) - await self.wsock.send_json(message, dumps=JSON_DUMP) + try: + await self.wsock.send_json(message, dumps=JSON_DUMP) + except TypeError as err: + _LOGGER.error('Unable to serialize to JSON: %s\n%s', + err, message) @callback def send_message_outside(self, message): diff --git a/homeassistant/remote.py b/homeassistant/remote.py index 566f37a621a..5a33bd58641 100644 --- a/homeassistant/remote.py +++ b/homeassistant/remote.py @@ -123,17 +123,7 @@ class JSONEncoder(json.JSONEncoder): elif hasattr(o, 'as_dict'): return o.as_dict() - try: - return json.JSONEncoder.default(self, o) - except TypeError: - # If the JSON serializer couldn't serialize it - # it might be a generator, convert it to a list - try: - return [self.default(child_obj) - for child_obj in o] - except TypeError: - # Ok, we're lost, cause the original error - return json.JSONEncoder.default(self, o) + return json.JSONEncoder.default(self, o) def validate_api(api): diff --git a/tests/components/http/test_view.py b/tests/components/http/test_view.py new file mode 100644 index 00000000000..ac0e23edd64 --- /dev/null +++ b/tests/components/http/test_view.py @@ -0,0 +1,15 @@ +"""Tests for Home Assistant View.""" +from aiohttp.web_exceptions import HTTPInternalServerError +import pytest + +from homeassistant.components.http.view import HomeAssistantView + + +async def test_invalid_json(caplog): + """Test trying to return invalid JSON.""" + view = HomeAssistantView() + + with pytest.raises(HTTPInternalServerError): + view.json(object) + + assert str(object) in caplog.text From d028c33e7f37fafcdc97e4161c3fa67e6be31e71 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Tue, 13 Mar 2018 00:12:21 +0100 Subject: [PATCH 061/220] Disable Monkey Patch for 3.6.3+ (#13150) --- homeassistant/__main__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/__main__.py b/homeassistant/__main__.py index 3f6e0ef220f..aa966027922 100644 --- a/homeassistant/__main__.py +++ b/homeassistant/__main__.py @@ -335,7 +335,8 @@ def main() -> int: """Start Home Assistant.""" validate_python() - if os.environ.get('HASS_NO_MONKEY') != '1': + monkey_patch_needed = sys.version_info[:3] < (3, 6, 3) + if monkey_patch_needed and os.environ.get('HASS_NO_MONKEY') != '1': if sys.version_info[:2] >= (3, 6): monkey_patch.disable_c_asyncio() monkey_patch.patch_weakref_tasks() From 989638b2668e1d0b6bc45581755fdf401c3df649 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Tue, 13 Mar 2018 02:22:48 +0100 Subject: [PATCH 062/220] Upgrade Sphinx to 1.7.1 (#13127) --- requirements_docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_docs.txt b/requirements_docs.txt index 60946fd00a8..bb0d30462ce 100644 --- a/requirements_docs.txt +++ b/requirements_docs.txt @@ -1,3 +1,3 @@ -Sphinx==1.7.0 +Sphinx==1.7.1 sphinx-autodoc-typehints==1.2.5 sphinx-autodoc-annotation==1.0.post1 From 75fb8ef98bd0ca854eacf2f9ebf4fa2a5b7cb66e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Tue, 13 Mar 2018 07:04:27 +0100 Subject: [PATCH 063/220] upgrade tibber libary to 0.4.0 to use aiohttp 3.0 (#13164) --- homeassistant/components/sensor/tibber.py | 4 ++-- requirements_all.txt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sensor/tibber.py b/homeassistant/components/sensor/tibber.py index a5f490c8d51..8c8ffdfd954 100644 --- a/homeassistant/components/sensor/tibber.py +++ b/homeassistant/components/sensor/tibber.py @@ -20,7 +20,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.entity import Entity from homeassistant.util import dt as dt_util -REQUIREMENTS = ['pyTibber==0.3.2'] +REQUIREMENTS = ['pyTibber==0.4.0'] _LOGGER = logging.getLogger(__name__) @@ -76,7 +76,7 @@ class TibberSensor(Entity): price_time = dt_util.as_utc(dt_util.parse_datetime(key)) time_diff = (now - price_time).total_seconds()/60 if time_diff >= 0 and time_diff < 60: - self._state = round(price_total, 2) + self._state = round(price_total, 3) self._last_updated = key return True return False diff --git a/requirements_all.txt b/requirements_all.txt index d53f4e39b36..fab0675d1e6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -645,7 +645,7 @@ pyHS100==0.3.0 pyRFXtrx==0.21.1 # homeassistant.components.sensor.tibber -pyTibber==0.3.2 +pyTibber==0.4.0 # homeassistant.components.switch.dlink pyW215==0.6.0 From 53351423dd0f14ddfdd99dd9c0afea0ee7530aab Mon Sep 17 00:00:00 2001 From: Jesse Hills Date: Tue, 13 Mar 2018 20:29:20 +1300 Subject: [PATCH 064/220] Change iglo port to cv.port validator (#13163) --- homeassistant/components/light/iglo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/light/iglo.py b/homeassistant/components/light/iglo.py index 9717993f77d..c7de8d8bede 100644 --- a/homeassistant/components/light/iglo.py +++ b/homeassistant/components/light/iglo.py @@ -27,7 +27,7 @@ DEFAULT_PORT = 8080 PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, }) From 8a1687accb31a4bf6b42ccc19e36a0a259563c42 Mon Sep 17 00:00:00 2001 From: Kane610 Date: Tue, 13 Mar 2018 08:47:45 +0100 Subject: [PATCH 065/220] deConz rewrite to use async await syntax (#13151) * Rewrite to use async await syntax * Fix hound comment --- .../components/binary_sensor/deconz.py | 9 ++-- homeassistant/components/deconz/__init__.py | 42 ++++++++----------- homeassistant/components/light/deconz.py | 19 ++++----- homeassistant/components/scene/deconz.py | 14 +++---- homeassistant/components/sensor/deconz.py | 12 ++---- requirements_all.txt | 2 +- 6 files changed, 37 insertions(+), 61 deletions(-) diff --git a/homeassistant/components/binary_sensor/deconz.py b/homeassistant/components/binary_sensor/deconz.py index 28e78db90ec..1effcf1800a 100644 --- a/homeassistant/components/binary_sensor/deconz.py +++ b/homeassistant/components/binary_sensor/deconz.py @@ -4,8 +4,6 @@ Support for deCONZ binary sensor. For more details about this component, please refer to the documentation at https://home-assistant.io/components/binary_sensor.deconz/ """ -import asyncio - from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.components.deconz import ( DOMAIN as DATA_DECONZ, DATA_DECONZ_ID) @@ -15,8 +13,8 @@ from homeassistant.core import callback DEPENDENCIES = ['deconz'] -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_devices, discovery_info=None): +async def async_setup_platform(hass, config, async_add_devices, + discovery_info=None): """Set up the deCONZ binary sensor.""" if discovery_info is None: return @@ -39,8 +37,7 @@ class DeconzBinarySensor(BinarySensorDevice): """Set up sensor and add update callback to get data from websocket.""" self._sensor = sensor - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Subscribe sensors events.""" self._sensor.register_async_callback(self.async_update_callback) self.hass.data[DATA_DECONZ_ID][self.entity_id] = self._sensor.deconz_id diff --git a/homeassistant/components/deconz/__init__.py b/homeassistant/components/deconz/__init__.py index 18197b84b61..de6d3e89859 100644 --- a/homeassistant/components/deconz/__init__.py +++ b/homeassistant/components/deconz/__init__.py @@ -4,8 +4,6 @@ Support for deCONZ devices. For more details about this component, please refer to the documentation at https://home-assistant.io/components/deconz/ """ - -import asyncio import logging import voluptuous as vol @@ -19,7 +17,7 @@ from homeassistant.helpers import discovery from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.util.json import load_json, save_json -REQUIREMENTS = ['pydeconz==30'] +REQUIREMENTS = ['pydeconz==31'] _LOGGER = logging.getLogger(__name__) @@ -57,30 +55,28 @@ Unlock your deCONZ gateway to register with Home Assistant. """ -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Set up services and configuration for deCONZ component.""" result = False - config_file = yield from hass.async_add_job( + config_file = await hass.async_add_job( load_json, hass.config.path(CONFIG_FILE)) - @asyncio.coroutine - def async_deconz_discovered(service, discovery_info): + async def async_deconz_discovered(service, discovery_info): """Call when deCONZ gateway has been found.""" deconz_config = {} deconz_config[CONF_HOST] = discovery_info.get(CONF_HOST) deconz_config[CONF_PORT] = discovery_info.get(CONF_PORT) - yield from async_request_configuration(hass, config, deconz_config) + await async_request_configuration(hass, config, deconz_config) if config_file: - result = yield from async_setup_deconz(hass, config, config_file) + result = await async_setup_deconz(hass, config, config_file) if not result and DOMAIN in config and CONF_HOST in config[DOMAIN]: deconz_config = config[DOMAIN] if CONF_API_KEY in deconz_config: - result = yield from async_setup_deconz(hass, config, deconz_config) + result = await async_setup_deconz(hass, config, deconz_config) else: - yield from async_request_configuration(hass, config, deconz_config) + await async_request_configuration(hass, config, deconz_config) return True if not result: @@ -89,8 +85,7 @@ def async_setup(hass, config): return True -@asyncio.coroutine -def async_setup_deconz(hass, config, deconz_config): +async def async_setup_deconz(hass, config, deconz_config): """Set up a deCONZ session. Load config, group, light and sensor data for server information. @@ -100,7 +95,7 @@ def async_setup_deconz(hass, config, deconz_config): from pydeconz import DeconzSession websession = async_get_clientsession(hass) deconz = DeconzSession(hass.loop, websession, **deconz_config) - result = yield from deconz.async_load_parameters() + result = await deconz.async_load_parameters() if result is False: _LOGGER.error("Failed to communicate with deCONZ") return False @@ -113,8 +108,7 @@ def async_setup_deconz(hass, config, deconz_config): hass, component, DOMAIN, {}, config)) deconz.start() - @asyncio.coroutine - def async_configure(call): + async def async_configure(call): """Set attribute of device in deCONZ. Field is a string representing a specific device in deCONZ @@ -140,7 +134,7 @@ def async_setup_deconz(hass, config, deconz_config): if field is None: _LOGGER.error('Could not find the entity %s', entity_id) return - yield from deconz.async_put_state(field, data) + await deconz.async_put_state(field, data) hass.services.async_register( DOMAIN, 'configure', async_configure, schema=SERVICE_SCHEMA) @@ -159,21 +153,19 @@ def async_setup_deconz(hass, config, deconz_config): return True -@asyncio.coroutine -def async_request_configuration(hass, config, deconz_config): +async def async_request_configuration(hass, config, deconz_config): """Request configuration steps from the user.""" configurator = hass.components.configurator - @asyncio.coroutine - def async_configuration_callback(data): + async def async_configuration_callback(data): """Set up actions to do when our configuration callback is called.""" from pydeconz.utils import async_get_api_key - api_key = yield from async_get_api_key(hass.loop, **deconz_config) + api_key = await async_get_api_key(hass.loop, **deconz_config) if api_key: deconz_config[CONF_API_KEY] = api_key - result = yield from async_setup_deconz(hass, config, deconz_config) + result = await async_setup_deconz(hass, config, deconz_config) if result: - yield from hass.async_add_job( + await hass.async_add_job( save_json, hass.config.path(CONFIG_FILE), deconz_config) configurator.async_request_done(request_id) return diff --git a/homeassistant/components/light/deconz.py b/homeassistant/components/light/deconz.py index 0eef5a868b4..a3e54434109 100644 --- a/homeassistant/components/light/deconz.py +++ b/homeassistant/components/light/deconz.py @@ -4,8 +4,6 @@ Support for deCONZ light. For more details about this component, please refer to the documentation at https://home-assistant.io/components/light.deconz/ """ -import asyncio - from homeassistant.components.deconz import ( DOMAIN as DATA_DECONZ, DATA_DECONZ_ID) from homeassistant.components.light import ( @@ -19,8 +17,8 @@ from homeassistant.util.color import color_RGB_to_xy DEPENDENCIES = ['deconz'] -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_devices, discovery_info=None): +async def async_setup_platform(hass, config, async_add_devices, + discovery_info=None): """Set up the deCONZ light.""" if discovery_info is None: return @@ -59,8 +57,7 @@ class DeconzLight(Light): if self._light.effect is not None: self._features |= SUPPORT_EFFECT - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Subscribe to lights events.""" self._light.register_async_callback(self.async_update_callback) self.hass.data[DATA_DECONZ_ID][self.entity_id] = self._light.deconz_id @@ -120,8 +117,7 @@ class DeconzLight(Light): """No polling needed.""" return False - @asyncio.coroutine - def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): """Turn on light.""" data = {'on': True} @@ -157,10 +153,9 @@ class DeconzLight(Light): else: data['effect'] = 'none' - yield from self._light.async_set_state(data) + await self._light.async_set_state(data) - @asyncio.coroutine - def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs): """Turn off light.""" data = {'on': False} @@ -176,4 +171,4 @@ class DeconzLight(Light): data['alert'] = 'lselect' del data['on'] - yield from self._light.async_set_state(data) + await self._light.async_set_state(data) diff --git a/homeassistant/components/scene/deconz.py b/homeassistant/components/scene/deconz.py index db81d84c2b7..dffc7720776 100644 --- a/homeassistant/components/scene/deconz.py +++ b/homeassistant/components/scene/deconz.py @@ -4,8 +4,6 @@ Support for deCONZ scenes. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/scene.deconz/ """ -import asyncio - from homeassistant.components.deconz import ( DOMAIN as DATA_DECONZ, DATA_DECONZ_ID) from homeassistant.components.scene import Scene @@ -13,8 +11,8 @@ from homeassistant.components.scene import Scene DEPENDENCIES = ['deconz'] -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_devices, discovery_info=None): +async def async_setup_platform(hass, config, async_add_devices, + discovery_info=None): """Set up scenes for deCONZ component.""" if discovery_info is None: return @@ -34,15 +32,13 @@ class DeconzScene(Scene): """Set up a scene.""" self._scene = scene - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Subscribe to sensors events.""" self.hass.data[DATA_DECONZ_ID][self.entity_id] = self._scene.deconz_id - @asyncio.coroutine - def async_activate(self): + async def async_activate(self): """Activate the scene.""" - yield from self._scene.async_set_state({}) + await self._scene.async_set_state({}) @property def name(self): diff --git a/homeassistant/components/sensor/deconz.py b/homeassistant/components/sensor/deconz.py index b60df1c6ac9..a3c2aa683dc 100644 --- a/homeassistant/components/sensor/deconz.py +++ b/homeassistant/components/sensor/deconz.py @@ -4,8 +4,6 @@ Support for deCONZ sensor. For more details about this component, please refer to the documentation at https://home-assistant.io/components/sensor.deconz/ """ -import asyncio - from homeassistant.components.deconz import ( DOMAIN as DATA_DECONZ, DATA_DECONZ_ID) from homeassistant.const import ATTR_BATTERY_LEVEL, CONF_EVENT, CONF_ID @@ -19,8 +17,8 @@ DEPENDENCIES = ['deconz'] ATTR_EVENT_ID = 'event_id' -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_devices, discovery_info=None): +async def async_setup_platform(hass, config, async_add_devices, + discovery_info=None): """Set up the deCONZ sensors.""" if discovery_info is None: return @@ -48,8 +46,7 @@ class DeconzSensor(Entity): """Set up sensor and add update callback to get data from websocket.""" self._sensor = sensor - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Subscribe to sensors events.""" self._sensor.register_async_callback(self.async_update_callback) self.hass.data[DATA_DECONZ_ID][self.entity_id] = self._sensor.deconz_id @@ -125,8 +122,7 @@ class DeconzBattery(Entity): self._device_class = 'battery' self._unit_of_measurement = "%" - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Subscribe to sensors events.""" self._device.register_async_callback(self.async_update_callback) self.hass.data[DATA_DECONZ_ID][self.entity_id] = self._device.deconz_id diff --git a/requirements_all.txt b/requirements_all.txt index fab0675d1e6..0ce8465df6d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -701,7 +701,7 @@ pycsspeechtts==1.0.2 pydaikin==0.4 # homeassistant.components.deconz -pydeconz==30 +pydeconz==31 # homeassistant.components.zwave pydispatcher==2.0.5 From d5612b5ccc4b30ed62022a5b042459ac03425b1e Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Tue, 13 Mar 2018 17:30:31 +0100 Subject: [PATCH 066/220] Upgrade holidays to 0.9.4 (#13167) --- homeassistant/components/binary_sensor/workday.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/binary_sensor/workday.py b/homeassistant/components/binary_sensor/workday.py index 58599d3d3de..f5a7324d351 100644 --- a/homeassistant/components/binary_sensor/workday.py +++ b/homeassistant/components/binary_sensor/workday.py @@ -17,7 +17,7 @@ import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['holidays==0.9.3'] +REQUIREMENTS = ['holidays==0.9.4'] # List of all countries currently supported by holidays # There seems to be no way to get the list out at runtime diff --git a/requirements_all.txt b/requirements_all.txt index 0ce8465df6d..c4c640b063a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -350,7 +350,7 @@ hikvision==0.4 hipnotify==1.0.8 # homeassistant.components.binary_sensor.workday -holidays==0.9.3 +holidays==0.9.4 # homeassistant.components.frontend home-assistant-frontend==20180310.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2f4768fd90b..dc0ca164ab2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -75,7 +75,7 @@ haversine==0.4.5 hbmqtt==0.9.1 # homeassistant.components.binary_sensor.workday -holidays==0.9.3 +holidays==0.9.4 # homeassistant.components.frontend home-assistant-frontend==20180310.0 From dd48fb04a33d238955fbbad7b47fcc1894693f2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Tue, 13 Mar 2018 22:51:10 +0200 Subject: [PATCH 067/220] upcloud: Provide unique ID for server entities (#13181) --- homeassistant/components/upcloud.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/upcloud.py b/homeassistant/components/upcloud.py index 40e4ceffed8..9de7f6c4444 100644 --- a/homeassistant/components/upcloud.py +++ b/homeassistant/components/upcloud.py @@ -116,6 +116,11 @@ class UpCloudServerEntity(Entity): self.uuid = uuid self.data = None + @property + def unique_id(self) -> str: + """Return unique ID for the entity.""" + return self.uuid + @property def name(self): """Return the name of the component.""" From 3e7a737bff8a5e7d941bc53ccb191dc9eeecea84 Mon Sep 17 00:00:00 2001 From: Andreas Wolter Date: Tue, 13 Mar 2018 21:54:09 +0100 Subject: [PATCH 068/220] Added IPAreaThermostat and an exception-list for HM_IGNORE_DISCOVERY_NODE (#13171) --- homeassistant/components/homematic/__init__.py | 9 +++++++-- homeassistant/components/sensor/homematic.py | 2 ++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/homematic/__init__.py b/homeassistant/components/homematic/__init__.py index 38ce712b9b0..b913b58864d 100644 --- a/homeassistant/components/homematic/__init__.py +++ b/homeassistant/components/homematic/__init__.py @@ -68,7 +68,7 @@ HM_DEVICE_TYPES = { 'WeatherStation', 'ThermostatWall2', 'TemperatureDiffSensor', 'TemperatureSensor', 'CO2Sensor', 'IPSwitchPowermeter', 'HMWIOSwitch', 'FillingLevel', 'ValveDrive', 'EcoLogic', 'IPThermostatWall', - 'IPSmoke', 'RFSiren', 'PresenceIP'], + 'IPSmoke', 'RFSiren', 'PresenceIP', 'IPAreaThermostat'], DISCOVER_CLIMATE: [ 'Thermostat', 'ThermostatWall', 'MAXThermostat', 'ThermostatWall2', 'MAXWallThermostat', 'IPThermostat', 'IPThermostatWall', @@ -86,6 +86,10 @@ HM_IGNORE_DISCOVERY_NODE = [ 'ACTUAL_HUMIDITY' ] +HM_IGNORE_DISCOVERY_NODE_EXCEPTIONS = { + 'ACTUAL_TEMPERATURE': ['IPAreaThermostat'], +} + HM_ATTRIBUTE_SUPPORT = { 'LOWBAT': ['battery', {0: 'High', 1: 'Low'}], 'LOW_BAT': ['battery', {0: 'High', 1: 'Low'}], @@ -505,7 +509,8 @@ def _get_devices(hass, discovery_type, keys, interface): # Generate options for 1...n elements with 1...n parameters for param, channels in metadata.items(): - if param in HM_IGNORE_DISCOVERY_NODE: + if param in HM_IGNORE_DISCOVERY_NODE and class_name not in \ + HM_IGNORE_DISCOVERY_NODE_EXCEPTIONS.get(param, []): continue # Add devices diff --git a/homeassistant/components/sensor/homematic.py b/homeassistant/components/sensor/homematic.py index 936533422bb..350f1e2eb59 100644 --- a/homeassistant/components/sensor/homematic.py +++ b/homeassistant/components/sensor/homematic.py @@ -35,6 +35,7 @@ HM_STATE_HA_CAST = { HM_UNIT_HA_CAST = { 'HUMIDITY': '%', 'TEMPERATURE': '°C', + 'ACTUAL_TEMPERATURE': '°C', 'BRIGHTNESS': '#', 'POWER': 'W', 'CURRENT': 'mA', @@ -57,6 +58,7 @@ HM_ICON_HA_CAST = { 'WIND_SPEED': 'mdi:weather-windy', 'HUMIDITY': 'mdi:water-percent', 'TEMPERATURE': 'mdi:thermometer', + 'ACTUAL_TEMPERATURE': 'mdi:thermometer', 'LUX': 'mdi:weather-sunny', 'BRIGHTNESS': 'mdi:invert-colors', 'POWER': 'mdi:flash-red-eye', From 5958e6a60ff16cae8c6d345cf575f37fb4782503 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Tue, 13 Mar 2018 21:56:16 +0100 Subject: [PATCH 069/220] Improve MQTT failed connection error message (#13184) --- homeassistant/components/mqtt/__init__.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 590e16747c5..b81a4fc16a7 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -515,16 +515,20 @@ class MQTT(object): This method is a coroutine. """ result = None # type: int - result = await self.hass.async_add_job( - self._mqttc.connect, self.broker, self.port, self.keepalive) + try: + result = await self.hass.async_add_job( + self._mqttc.connect, self.broker, self.port, self.keepalive) + except OSError as err: + _LOGGER.error('Failed to connect due to exception: %s', err) + return False if result != 0: import paho.mqtt.client as mqtt _LOGGER.error('Failed to connect: %s', mqtt.error_string(result)) - else: - self._mqttc.loop_start() + return False - return not result + self._mqttc.loop_start() + return True @callback def async_disconnect(self): From 4c9e7c2da4f0627f98f5a7681b1c3f3a76c20f0e Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Tue, 13 Mar 2018 21:57:04 +0100 Subject: [PATCH 070/220] Upgrade pytest to 3.4.2 (#13169) * Upgrade pytest to 3.4.2 * Upgrade pytest-sugar to 0.9.1 --- 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 34506f5c40e..fc9e113e97c 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -11,7 +11,7 @@ pydocstyle==1.1.1 pylint==1.8.2 pytest-aiohttp==0.3.0 pytest-cov==2.5.1 -pytest-sugar==0.9.0 +pytest-sugar==0.9.1 pytest-timeout>=1.2.1 -pytest==3.3.1 +pytest==3.4.2 requests_mock==1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index dc0ca164ab2..2c785bee3af 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -12,9 +12,9 @@ pydocstyle==1.1.1 pylint==1.8.2 pytest-aiohttp==0.3.0 pytest-cov==2.5.1 -pytest-sugar==0.9.0 +pytest-sugar==0.9.1 pytest-timeout>=1.2.1 -pytest==3.3.1 +pytest==3.4.2 requests_mock==1.4 From 71baa6532e78c8c5392b72de0358fd69288ca391 Mon Sep 17 00:00:00 2001 From: Alok Saboo Date: Tue, 13 Mar 2018 17:12:28 -0400 Subject: [PATCH 071/220] Revert throttle Arlo api calls (#13174) --- homeassistant/components/arlo.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/homeassistant/components/arlo.py b/homeassistant/components/arlo.py index 77201e5ead9..7e51ec8c045 100644 --- a/homeassistant/components/arlo.py +++ b/homeassistant/components/arlo.py @@ -5,13 +5,11 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/arlo/ """ import logging -from datetime import timedelta import voluptuous as vol from requests.exceptions import HTTPError, ConnectTimeout from homeassistant.helpers import config_validation as cv -from homeassistant.util import Throttle from homeassistant.const import CONF_USERNAME, CONF_PASSWORD REQUIREMENTS = ['pyarlo==0.1.2'] @@ -47,7 +45,6 @@ def setup(hass, config): arlo = PyArlo(username, password, preload=False) if not arlo.is_connected: return False - arlo.update = Throttle(timedelta(seconds=10))(arlo.update) hass.data[DATA_ARLO] = arlo except (ConnectTimeout, HTTPError) as ex: _LOGGER.error("Unable to connect to Netgear Arlo: %s", str(ex)) From 24a9da85c01c1f19452af4e82fd92838954fd8d8 Mon Sep 17 00:00:00 2001 From: Jon Maddox Date: Tue, 13 Mar 2018 17:14:02 -0400 Subject: [PATCH 072/220] Channels clean ups (#12967) * already in the default schema * these are already globally disabled * require a single entity id * remove unused import * w h i t e s p a c e * actually keep it * it is a string * use a generator expression * :lipstick: * Revert ":lipstick:" This reverts commit 81c08bb7323ec1242ee733bea8421c42243f6f63. * Revert "actually keep it" This reverts commit 0d92d3afb235876bf0af3b46e09202451ebe88c8. * Revert "remove unused import" This reverts commit 8a166208e496883f71e42d65dd9fab5f7c3d418c. * Revert "already in the default schema" This reverts commit 9173de4fd3bc962ad9f57fc966b1f11da0abb69e. * we're already ensuring defaults with the platform schema --- .../components/media_player/channels.py | 37 +++++++++---------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/media_player/channels.py b/homeassistant/components/media_player/channels.py index eda47237b44..480e5152c8e 100644 --- a/homeassistant/components/media_player/channels.py +++ b/homeassistant/components/media_player/channels.py @@ -45,7 +45,7 @@ SERVICE_SEEK_BY = 'channels_seek_by' ATTR_SECONDS = 'seconds' CHANNELS_SCHEMA = vol.Schema({ - vol.Required(ATTR_ENTITY_ID): cv.entity_ids, + vol.Required(ATTR_ENTITY_ID): cv.entity_id, }) CHANNELS_SEEK_BY_SCHEMA = CHANNELS_SCHEMA.extend({ @@ -55,14 +55,12 @@ CHANNELS_SEEK_BY_SCHEMA = CHANNELS_SCHEMA.extend({ REQUIREMENTS = ['pychannels==1.0.0'] -# pylint: disable=unused-argument, abstract-method -# pylint: disable=too-many-instance-attributes def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the Channels platform.""" device = ChannelsPlayer( - config.get('name', DEFAULT_NAME), + config.get('name'), config.get(CONF_HOST), - config.get(CONF_PORT, DEFAULT_PORT) + config.get(CONF_PORT) ) if DATA_CHANNELS not in hass.data: @@ -73,22 +71,23 @@ def setup_platform(hass, config, add_devices, discovery_info=None): def service_handler(service): """Handler for services.""" - entity_ids = service.data.get(ATTR_ENTITY_ID) + entity_id = service.data.get(ATTR_ENTITY_ID) - if entity_ids: - devices = [device for device in hass.data[DATA_CHANNELS] - if device.entity_id in entity_ids] - else: - devices = hass.data[DATA_CHANNELS] + device = next((device for device in hass.data[DATA_CHANNELS] if + device.entity_id == entity_id), None) - for device in devices: - if service.service == SERVICE_SEEK_FORWARD: - device.seek_forward() - elif service.service == SERVICE_SEEK_BACKWARD: - device.seek_backward() - elif service.service == SERVICE_SEEK_BY: - seconds = service.data.get('seconds') - device.seek_by(seconds) + if device is None: + _LOGGER.warning("Unable to find Channels with entity_id: %s", + entity_id) + return + + if service.service == SERVICE_SEEK_FORWARD: + device.seek_forward() + elif service.service == SERVICE_SEEK_BACKWARD: + device.seek_backward() + elif service.service == SERVICE_SEEK_BY: + seconds = service.data.get('seconds') + device.seek_by(seconds) hass.services.register( DOMAIN, SERVICE_SEEK_FORWARD, service_handler, From 0ef43400990b61387503e42bb608eec06411ba80 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Wed, 14 Mar 2018 04:50:08 +0100 Subject: [PATCH 073/220] Fix freegeoip (#13193) --- homeassistant/util/location.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/util/location.py b/homeassistant/util/location.py index 0cd0b14d3ab..dae8ed17dc9 100644 --- a/homeassistant/util/location.py +++ b/homeassistant/util/location.py @@ -10,7 +10,7 @@ from typing import Any, Optional, Tuple, Dict import requests ELEVATION_URL = 'http://maps.googleapis.com/maps/api/elevation/json' -FREEGEO_API = 'https://freegeoip.io/json/' +FREEGEO_API = 'https://freegeoip.net/json/' IP_API = 'http://ip-api.com/json' # Constants from https://github.com/maurycyp/vincenty From cfded7eab91a82aaed199802f773a95ec5e534c2 Mon Sep 17 00:00:00 2001 From: JC Connell Date: Wed, 14 Mar 2018 03:01:10 -0400 Subject: [PATCH 074/220] Python Spotcrime sensor requires API key, fixes include/exclude (#12926) * Add spotcrime.py to dev * Modify sensor to accept user API key * Update Spotcrime to 1.0.3 in requirements_all.txt * Fix line 76 (97 > 79 characters) * Fix lint errors --- homeassistant/components/sensor/spotcrime.py | 19 ++++++++++++------- requirements_all.txt | 2 +- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/sensor/spotcrime.py b/homeassistant/components/sensor/spotcrime.py index 169bcc5f867..08177c9a7b9 100644 --- a/homeassistant/components/sensor/spotcrime.py +++ b/homeassistant/components/sensor/spotcrime.py @@ -12,14 +12,15 @@ import logging import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import ( - CONF_INCLUDE, CONF_EXCLUDE, CONF_NAME, CONF_LATITUDE, CONF_LONGITUDE, - ATTR_ATTRIBUTION, ATTR_LATITUDE, ATTR_LONGITUDE, CONF_RADIUS) +from homeassistant.const import (CONF_API_KEY, CONF_INCLUDE, CONF_EXCLUDE, + CONF_NAME, CONF_LATITUDE, CONF_LONGITUDE, + ATTR_ATTRIBUTION, ATTR_LATITUDE, + ATTR_LONGITUDE, CONF_RADIUS) from homeassistant.helpers.entity import Entity from homeassistant.util import slugify import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['spotcrime==1.0.2'] +REQUIREMENTS = ['spotcrime==1.0.3'] _LOGGER = logging.getLogger(__name__) @@ -34,6 +35,7 @@ SCAN_INTERVAL = timedelta(minutes=30) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_NAME): cv.string, vol.Required(CONF_RADIUS): vol.Coerce(float), + vol.Required(CONF_API_KEY): cv.string, vol.Inclusive(CONF_LATITUDE, 'coordinates'): cv.latitude, vol.Inclusive(CONF_LONGITUDE, 'coordinates'): cv.longitude, vol.Optional(CONF_DAYS, default=DEFAULT_DAYS): cv.positive_int, @@ -49,28 +51,31 @@ def setup_platform(hass, config, add_devices, discovery_info=None): longitude = config.get(CONF_LONGITUDE, hass.config.longitude) name = config[CONF_NAME] radius = config[CONF_RADIUS] + api_key = config[CONF_API_KEY] days = config.get(CONF_DAYS) include = config.get(CONF_INCLUDE) exclude = config.get(CONF_EXCLUDE) add_devices([SpotCrimeSensor( name, latitude, longitude, radius, include, - exclude, days)], True) + exclude, api_key, days)], True) class SpotCrimeSensor(Entity): """Representation of a Spot Crime Sensor.""" def __init__(self, name, latitude, longitude, radius, - include, exclude, days): + include, exclude, api_key, days): """Initialize the Spot Crime sensor.""" import spotcrime self._name = name self._include = include self._exclude = exclude + self.api_key = api_key self.days = days self._spotcrime = spotcrime.SpotCrime( - (latitude, longitude), radius, None, None, self.days) + (latitude, longitude), radius, self._include, + self._exclude, self.api_key, self.days) self._attributes = None self._state = None self._previous_incidents = set() diff --git a/requirements_all.txt b/requirements_all.txt index c4c640b063a..9ad1be678fa 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1153,7 +1153,7 @@ somecomfort==0.5.0 speedtest-cli==2.0.0 # homeassistant.components.sensor.spotcrime -spotcrime==1.0.2 +spotcrime==1.0.3 # homeassistant.components.recorder # homeassistant.scripts.db_migrator From 6310deb5c20b6e4cd4996582426f486081421359 Mon Sep 17 00:00:00 2001 From: Mark Perdue Date: Wed, 14 Mar 2018 03:10:47 -0400 Subject: [PATCH 075/220] Add new platform for VeSync switches (#13000) * Added vesync platform Support for power toggling, current power, and daily energy kWh * Adds vesync to requirements file. * Reorder vesync item in requirements_all.txt from gen_requirements_all * Removes unnecessary global values that are not used in this component * Removes try/catch from setup_platform -no throws. Guard check login() * Remove unnecessary boolean convert * Fix indentation of log messages --- homeassistant/components/switch/vesync.py | 104 ++++++++++++++++++++++ requirements_all.txt | 3 + 2 files changed, 107 insertions(+) create mode 100644 homeassistant/components/switch/vesync.py diff --git a/homeassistant/components/switch/vesync.py b/homeassistant/components/switch/vesync.py new file mode 100644 index 00000000000..fbc73545e19 --- /dev/null +++ b/homeassistant/components/switch/vesync.py @@ -0,0 +1,104 @@ +""" +Support for Etekcity VeSync switches. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/switch.vesync/ +""" +import logging +import voluptuous as vol +from homeassistant.components.switch import (SwitchDevice, PLATFORM_SCHEMA) +from homeassistant.const import (CONF_USERNAME, CONF_PASSWORD) +import homeassistant.helpers.config_validation as cv + + +REQUIREMENTS = ['pyvesync==0.1.1'] + +_LOGGER = logging.getLogger(__name__) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the VeSync switch platform.""" + from pyvesync.vesync import VeSync + + switches = [] + + manager = VeSync(config.get(CONF_USERNAME), config.get(CONF_PASSWORD)) + + if not manager.login(): + _LOGGER.error("Unable to login to VeSync") + return + + manager.update() + + if manager.devices is not None and manager.devices: + if len(manager.devices) == 1: + count_string = 'switch' + else: + count_string = 'switches' + + _LOGGER.info("Discovered %d VeSync %s", + len(manager.devices), count_string) + + for switch in manager.devices: + switches.append(VeSyncSwitchHA(switch)) + _LOGGER.info("Added a VeSync switch named '%s'", + switch.device_name) + else: + _LOGGER.info("No VeSync devices found") + + add_devices(switches) + + +class VeSyncSwitchHA(SwitchDevice): + """Representation of a VeSync switch.""" + + def __init__(self, plug): + """Initialize the VeSync switch device.""" + self.smartplug = plug + + @property + def unique_id(self): + """Return the ID of this switch.""" + return self.smartplug.cid + + @property + def name(self): + """Return the name of the switch.""" + return self.smartplug.device_name + + @property + def current_power_w(self): + """Return the current power usage in W.""" + return self.smartplug.get_power() + + @property + def today_energy_kwh(self): + """Return the today total energy usage in kWh.""" + return self.smartplug.get_kwh_today() + + @property + def available(self) -> bool: + """Return True if switch is available.""" + return self.smartplug.connection_status == "online" + + @property + def is_on(self): + """Return True if switch is on.""" + return self.smartplug.device_status == "on" + + def turn_on(self, **kwargs): + """Turn the switch on.""" + self.smartplug.turn_on() + + def turn_off(self, **kwargs): + """Turn the switch off.""" + self.smartplug.turn_off() + + def update(self): + """Handle data changes for node values.""" + self.smartplug.update() diff --git a/requirements_all.txt b/requirements_all.txt index 9ad1be678fa..1733f0d9a7d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1023,6 +1023,9 @@ pyunifi==2.13 # homeassistant.components.vera pyvera==0.2.42 +# homeassistant.components.switch.vesync +pyvesync==0.1.1 + # homeassistant.components.media_player.vizio pyvizio==0.0.2 From 948f29544ac5a8d5ce280aac765ef75b8b043bf7 Mon Sep 17 00:00:00 2001 From: Vincent Van Den Berghe Date: Wed, 14 Mar 2018 08:14:36 +0100 Subject: [PATCH 076/220] Fixed SI units for current consumption (#13190) --- homeassistant/components/sensor/smappee.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/sensor/smappee.py b/homeassistant/components/sensor/smappee.py index 51595d19b1a..c59798d16d7 100644 --- a/homeassistant/components/sensor/smappee.py +++ b/homeassistant/components/sensor/smappee.py @@ -21,17 +21,17 @@ SENSOR_TYPES = { 'active_power': ['Active Power', 'mdi:power-plug', 'local', 'W', 'active_power'], 'current': - ['Current', 'mdi:gauge', 'local', 'Amps', 'current'], + ['Current', 'mdi:gauge', 'local', 'A', 'current'], 'voltage': ['Voltage', 'mdi:gauge', 'local', 'V', 'voltage'], 'active_cosfi': ['Power Factor', 'mdi:gauge', 'local', '%', 'active_cosfi'], 'alwayson_today': - ['Always On Today', 'mdi:gauge', 'remote', 'kW', 'alwaysOn'], + ['Always On Today', 'mdi:gauge', 'remote', 'kWh', 'alwaysOn'], 'solar_today': - ['Solar Today', 'mdi:white-balance-sunny', 'remote', 'kW', 'solar'], + ['Solar Today', 'mdi:white-balance-sunny', 'remote', 'kWh', 'solar'], 'power_today': - ['Power Today', 'mdi:power-plug', 'remote', 'kW', 'consumption'] + ['Power Today', 'mdi:power-plug', 'remote', 'kWh', 'consumption'] } SCAN_INTERVAL = timedelta(seconds=30) From b6bed1dfabdf4ab59727ef5fc6ffb8571172d379 Mon Sep 17 00:00:00 2001 From: Dan Nixon Date: Wed, 14 Mar 2018 07:47:45 +0000 Subject: [PATCH 077/220] Report swap in MiB (#13148) It makes sense to report swap and memory in the same unit and MiB is more useful considering Home Assistant may be running on lower end hardware (Raspberry Pi for example) where 100MiB resolution is not adequate. --- homeassistant/components/sensor/systemmonitor.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/sensor/systemmonitor.py b/homeassistant/components/sensor/systemmonitor.py index 79d5c261b88..2f970796fe1 100644 --- a/homeassistant/components/sensor/systemmonitor.py +++ b/homeassistant/components/sensor/systemmonitor.py @@ -42,8 +42,8 @@ SENSOR_TYPES = { 'process': ['Process', ' ', 'mdi:memory'], 'processor_use': ['Processor use', '%', 'mdi:memory'], 'since_last_boot': ['Since last boot', '', 'mdi:clock'], - 'swap_free': ['Swap free', 'GiB', 'mdi:harddisk'], - 'swap_use': ['Swap use', 'GiB', 'mdi:harddisk'], + 'swap_free': ['Swap free', 'MiB', 'mdi:harddisk'], + 'swap_use': ['Swap use', 'MiB', 'mdi:harddisk'], 'swap_use_percent': ['Swap use (percent)', '%', 'mdi:harddisk'], } @@ -135,9 +135,9 @@ class SystemMonitorSensor(Entity): elif self.type == 'swap_use_percent': self._state = psutil.swap_memory().percent elif self.type == 'swap_use': - self._state = round(psutil.swap_memory().used / 1024**3, 1) + self._state = round(psutil.swap_memory().used / 1024**2, 1) elif self.type == 'swap_free': - self._state = round(psutil.swap_memory().free / 1024**3, 1) + self._state = round(psutil.swap_memory().free / 1024**2, 1) elif self.type == 'processor_use': self._state = round(psutil.cpu_percent(interval=None)) elif self.type == 'process': From c48c8710b7f34b18ad946aadedb59262fbfcd6ba Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Wed, 14 Mar 2018 13:22:38 +0100 Subject: [PATCH 078/220] Bugfix HomeKit: Error string values for temperature (#13162) --- homeassistant/components/homekit/thermostats.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/homekit/thermostats.py b/homeassistant/components/homekit/thermostats.py index 766a7e3585d..6d342273e8d 100644 --- a/homeassistant/components/homekit/thermostats.py +++ b/homeassistant/components/homekit/thermostats.py @@ -157,12 +157,12 @@ class Thermostat(HomeAccessory): # Update current temperature current_temp = new_state.attributes.get(ATTR_CURRENT_TEMPERATURE) - if current_temp is not None: + if isinstance(current_temp, (int, float)): self.char_current_temp.set_value(current_temp) # Update target temperature target_temp = new_state.attributes.get(ATTR_TEMPERATURE) - if target_temp is not None: + if isinstance(target_temp, (int, float)): if not self.temperature_flag_target_state: self.char_target_temp.set_value(target_temp, should_callback=False) From 7e2fc19f5a10cbac4260c0bd66f0814c5accf986 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 14 Mar 2018 11:39:38 -0700 Subject: [PATCH 079/220] Sort coveragerc --- .coveragerc | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/.coveragerc b/.coveragerc index 07d84523780..4149deee609 100644 --- a/.coveragerc +++ b/.coveragerc @@ -332,6 +332,7 @@ omit = homeassistant/components/camera/foscam.py homeassistant/components/camera/mjpeg.py homeassistant/components/camera/onvif.py + homeassistant/components/camera/proxy.py homeassistant/components/camera/ring.py homeassistant/components/camera/rpi_camera.py homeassistant/components/camera/synology.py @@ -403,20 +404,20 @@ omit = homeassistant/components/image_processing/dlib_face_detect.py homeassistant/components/image_processing/dlib_face_identify.py homeassistant/components/image_processing/seven_segments.py - homeassistant/components/keyboard.py homeassistant/components/keyboard_remote.py + homeassistant/components/keyboard.py homeassistant/components/light/avion.py homeassistant/components/light/blinksticklight.py homeassistant/components/light/blinkt.py - homeassistant/components/light/decora.py homeassistant/components/light/decora_wifi.py + homeassistant/components/light/decora.py homeassistant/components/light/flux_led.py homeassistant/components/light/greenwave.py homeassistant/components/light/hue.py homeassistant/components/light/hyperion.py homeassistant/components/light/iglo.py - homeassistant/components/light/lifx.py homeassistant/components/light/lifx_legacy.py + homeassistant/components/light/lifx.py homeassistant/components/light/limitlessled.py homeassistant/components/light/mystrom.py homeassistant/components/light/osramlightify.py @@ -442,6 +443,7 @@ omit = homeassistant/components/media_player/bluesound.py homeassistant/components/media_player/braviatv.py homeassistant/components/media_player/cast.py + homeassistant/components/media_player/channels.py homeassistant/components/media_player/clementine.py homeassistant/components/media_player/cmus.py homeassistant/components/media_player/denon.py @@ -482,8 +484,8 @@ omit = homeassistant/components/media_player/vlc.py homeassistant/components/media_player/volumio.py homeassistant/components/media_player/xiaomi_tv.py - homeassistant/components/media_player/yamaha.py homeassistant/components/media_player/yamaha_musiccast.py + homeassistant/components/media_player/yamaha.py homeassistant/components/media_player/ziggo_mediabox_xl.py homeassistant/components/mycroft.py homeassistant/components/notify/aws_lambda.py @@ -491,8 +493,8 @@ omit = homeassistant/components/notify/aws_sqs.py homeassistant/components/notify/ciscospark.py homeassistant/components/notify/clickatell.py - homeassistant/components/notify/clicksend.py homeassistant/components/notify/clicksend_tts.py + homeassistant/components/notify/clicksend.py homeassistant/components/notify/discord.py homeassistant/components/notify/free_mobile.py homeassistant/components/notify/gntp.py @@ -588,8 +590,8 @@ omit = homeassistant/components/sensor/haveibeenpwned.py homeassistant/components/sensor/hp_ilo.py homeassistant/components/sensor/htu21d.py - homeassistant/components/sensor/imap.py homeassistant/components/sensor/imap_email_content.py + homeassistant/components/sensor/imap.py homeassistant/components/sensor/influxdb.py homeassistant/components/sensor/irish_rail_transport.py homeassistant/components/sensor/kwb.py @@ -632,8 +634,8 @@ omit = homeassistant/components/sensor/scrape.py homeassistant/components/sensor/sense.py homeassistant/components/sensor/sensehat.py - homeassistant/components/sensor/serial.py homeassistant/components/sensor/serial_pm.py + homeassistant/components/sensor/serial.py homeassistant/components/sensor/shodan.py homeassistant/components/sensor/simulated.py homeassistant/components/sensor/skybeacon.py From e2029e397047338e9096e4f438d0ede301373e77 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 14 Mar 2018 12:05:17 -0700 Subject: [PATCH 080/220] Add vesync to coveragerc --- .coveragerc | 1 + 1 file changed, 1 insertion(+) diff --git a/.coveragerc b/.coveragerc index 4149deee609..5fd43d5aec7 100644 --- a/.coveragerc +++ b/.coveragerc @@ -699,6 +699,7 @@ omit = homeassistant/components/switch/telnet.py homeassistant/components/switch/tplink.py homeassistant/components/switch/transmission.py + homeassistant/components/switch/vesync.py homeassistant/components/switch/xiaomi_miio.py homeassistant/components/telegram_bot/* homeassistant/components/thingspeak.py From 7fc9ac09314a8e0a1cce326d55ab1ed577e22ce7 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Wed, 14 Mar 2018 20:07:50 +0100 Subject: [PATCH 081/220] Avoid Sonos error when joining with self (#13196) --- homeassistant/components/media_player/sonos.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/media_player/sonos.py b/homeassistant/components/media_player/sonos.py index 9ea33b4c396..edd7d17c67d 100644 --- a/homeassistant/components/media_player/sonos.py +++ b/homeassistant/components/media_player/sonos.py @@ -886,7 +886,8 @@ class SonosDevice(MediaPlayerDevice): self.soco.unjoin() for slave in slaves: - slave.soco.join(self.soco) + if slave.unique_id != self.unique_id: + slave.soco.join(self.soco) @soco_error() def unjoin(self): From ef7ce5eb1bbd62b31dd22a34e25be1dc50781ac2 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Wed, 14 Mar 2018 20:08:41 +0100 Subject: [PATCH 082/220] Ignore unsupported Sonos favorites (#13195) --- homeassistant/components/media_player/sonos.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/media_player/sonos.py b/homeassistant/components/media_player/sonos.py index edd7d17c67d..2a12b59e7c7 100644 --- a/homeassistant/components/media_player/sonos.py +++ b/homeassistant/components/media_player/sonos.py @@ -426,7 +426,17 @@ class SonosDevice(MediaPlayerDevice): self._play_mode = self.soco.play_mode self._night_sound = self.soco.night_mode self._speech_enhance = self.soco.dialog_mode - self._favorites = self.soco.music_library.get_sonos_favorites() + + self._favorites = [] + for fav in self.soco.music_library.get_sonos_favorites(): + # SoCo 0.14 raises a generic Exception on invalid xml in favorites. + # Filter those out now so our list is safe to use. + try: + if fav.reference.get_uri(): + self._favorites.append(fav) + # pylint: disable=broad-except + except Exception: + _LOGGER.debug("Ignoring invalid favorite '%s'", fav.title) def _subscribe_to_player_events(self): """Add event subscriptions.""" From a9917e7a56e4056d3bef59d810e637637a1317d5 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 14 Mar 2018 14:29:51 -0700 Subject: [PATCH 083/220] Fix history API (#13214) --- homeassistant/components/history.py | 10 +++++----- tests/components/test_history.py | 12 +++++++++++- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/history.py b/homeassistant/components/history.py index dd14bbf6811..8ab91b08a3d 100644 --- a/homeassistant/components/history.py +++ b/homeassistant/components/history.py @@ -239,15 +239,16 @@ def get_state(hass, utc_point_in_time, entity_id, run=None): def async_setup(hass, config): """Set up the history hooks.""" filters = Filters() - exclude = config[DOMAIN].get(CONF_EXCLUDE) + conf = config.get(DOMAIN, {}) + exclude = conf.get(CONF_EXCLUDE) if exclude: filters.excluded_entities = exclude.get(CONF_ENTITIES, []) filters.excluded_domains = exclude.get(CONF_DOMAINS, []) - include = config[DOMAIN].get(CONF_INCLUDE) + include = conf.get(CONF_INCLUDE) if include: filters.included_entities = include.get(CONF_ENTITIES, []) filters.included_domains = include.get(CONF_DOMAINS, []) - use_include_order = config[DOMAIN].get(CONF_ORDER) + use_include_order = conf.get(CONF_ORDER) hass.http.register_view(HistoryPeriodView(filters, use_include_order)) yield from hass.components.frontend.async_register_built_in_panel( @@ -308,7 +309,7 @@ class HistoryPeriodView(HomeAssistantView): result = yield from hass.async_add_job( get_significant_states, hass, start_time, end_time, entity_ids, self.filters, include_start_time_state) - result = result.values() + result = list(result.values()) if _LOGGER.isEnabledFor(logging.DEBUG): elapsed = time.perf_counter() - timer_start _LOGGER.debug( @@ -318,7 +319,6 @@ class HistoryPeriodView(HomeAssistantView): # by any entities explicitly included in the configuration. if self.use_include_order: - result = list(result) sorted_result = [] for order_entity in self.filters.included_entities: for state_list in result: diff --git a/tests/components/test_history.py b/tests/components/test_history.py index 4a759e7e0ac..be768f5ec69 100644 --- a/tests/components/test_history.py +++ b/tests/components/test_history.py @@ -4,7 +4,7 @@ from datetime import timedelta import unittest from unittest.mock import patch, sentinel -from homeassistant.setup import setup_component +from homeassistant.setup import setup_component, async_setup_component import homeassistant.core as ha import homeassistant.util.dt as dt_util from homeassistant.components import history, recorder @@ -481,3 +481,13 @@ class TestComponentHistory(unittest.TestCase): set_state(therm, 22, attributes={'current_temperature': 21, 'hidden': True}) return zero, four, states + + +async def test_fetch_period_api(hass, test_client): + """Test the fetch period view for history.""" + await hass.async_add_job(init_recorder_component, hass) + await async_setup_component(hass, 'history', {}) + client = await test_client(hass.http.app) + response = await client.get( + '/api/history/period/{}'.format(dt_util.utcnow().isoformat())) + assert response.status == 200 From e1a5e5a8ba5f922d5d223b0b2f97d5d9e60aba6f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 14 Mar 2018 15:07:37 -0700 Subject: [PATCH 084/220] Fix input_boolean Google Assistant serialize error (#13220) --- .../components/google_assistant/smart_home.py | 22 ++++++++++++++----- .../google_assistant/test_smart_home.py | 15 +++++++++++++ 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/google_assistant/smart_home.py b/homeassistant/components/google_assistant/smart_home.py index 48d24c00b97..834d40c367c 100644 --- a/homeassistant/components/google_assistant/smart_home.py +++ b/homeassistant/components/google_assistant/smart_home.py @@ -17,7 +17,16 @@ from homeassistant.core import callback from homeassistant.const import ( CONF_NAME, STATE_UNAVAILABLE, ATTR_SUPPORTED_FEATURES) from homeassistant.components import ( - switch, light, cover, media_player, group, fan, scene, script, climate, + climate, + cover, + fan, + group, + input_boolean, + light, + media_player, + scene, + script, + switch, ) from . import trait @@ -33,15 +42,16 @@ HANDLERS = Registry() _LOGGER = logging.getLogger(__name__) DOMAIN_TO_GOOGLE_TYPES = { + climate.DOMAIN: TYPE_THERMOSTAT, + cover.DOMAIN: TYPE_SWITCH, + fan.DOMAIN: TYPE_SWITCH, group.DOMAIN: TYPE_SWITCH, + input_boolean.DOMAIN: TYPE_SWITCH, + light.DOMAIN: TYPE_LIGHT, + media_player.DOMAIN: TYPE_SWITCH, scene.DOMAIN: TYPE_SCENE, script.DOMAIN: TYPE_SCENE, switch.DOMAIN: TYPE_SWITCH, - fan.DOMAIN: TYPE_SWITCH, - light.DOMAIN: TYPE_LIGHT, - cover.DOMAIN: TYPE_SWITCH, - media_player.DOMAIN: TYPE_SWITCH, - climate.DOMAIN: TYPE_THERMOSTAT, } diff --git a/tests/components/google_assistant/test_smart_home.py b/tests/components/google_assistant/test_smart_home.py index 8d139fa8211..24d74afa6da 100644 --- a/tests/components/google_assistant/test_smart_home.py +++ b/tests/components/google_assistant/test_smart_home.py @@ -1,4 +1,5 @@ """Test Google Smart Home.""" +from homeassistant.core import State from homeassistant.const import ( ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS) from homeassistant.setup import async_setup_component @@ -244,3 +245,17 @@ async def test_raising_error_trait(hass): }] } } + + +def test_serialize_input_boolean(): + """Test serializing an input boolean entity.""" + state = State('input_boolean.bla', 'on') + entity = sh._GoogleEntity(None, BASIC_CONFIG, state) + assert entity.sync_serialize() == { + 'id': 'input_boolean.bla', + 'attributes': {}, + 'name': {'name': 'bla'}, + 'traits': ['action.devices.traits.OnOff'], + 'type': 'action.devices.types.SWITCH', + 'willReportState': False, + } From 07f20676cbcd54771b39f62bb0e4aaa1fe8f4360 Mon Sep 17 00:00:00 2001 From: engrbm87 Date: Thu, 15 Mar 2018 01:03:40 +0200 Subject: [PATCH 085/220] Add notifications to downloader.py (#12961) * Update downloader.py Add persistent notification to alert when download is finished or in case of download failure. * Update downloader.py * Update downloader.py * Update downloader.py * Fire and event when download is requested Added 2 events to represent download completed and download failed. This will allow the user to trigger an automation based on the status of the download. * Update downloader.py * Update downloader.py replaced . with _ * Update downloader.py fixed linting errors --- homeassistant/components/downloader.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/homeassistant/components/downloader.py b/homeassistant/components/downloader.py index b7354b4f0a7..0d57740a83d 100644 --- a/homeassistant/components/downloader.py +++ b/homeassistant/components/downloader.py @@ -25,6 +25,8 @@ ATTR_OVERWRITE = 'overwrite' CONF_DOWNLOAD_DIR = 'download_dir' DOMAIN = 'downloader' +DOWNLOAD_FAILED_EVENT = 'download_failed' +DOWNLOAD_COMPLETED_EVENT = 'download_completed' SERVICE_DOWNLOAD_FILE = 'download_file' @@ -133,9 +135,19 @@ def setup(hass, config): fil.write(chunk) _LOGGER.debug("Downloading of %s done", url) + hass.bus.fire( + "{}_{}".format(DOMAIN, DOWNLOAD_COMPLETED_EVENT), { + 'url': url, + 'filename': filename + }) except requests.exceptions.ConnectionError: _LOGGER.exception("ConnectionError occurred for %s", url) + hass.bus.fire( + "{}_{}".format(DOMAIN, DOWNLOAD_FAILED_EVENT), { + 'url': url, + 'filename': filename + }) # Remove file if we started downloading but failed if final_path and os.path.isfile(final_path): From be2e202618879f141be020a7d11685ed6624fb34 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 14 Mar 2018 16:13:43 -0700 Subject: [PATCH 086/220] Bump frontend to 20180315.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 1f5a7576302..153d1f6564e 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -24,7 +24,7 @@ from homeassistant.core import callback from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass -REQUIREMENTS = ['home-assistant-frontend==20180310.0'] +REQUIREMENTS = ['home-assistant-frontend==20180315.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] From 64f18c62f4af12cc98e7754a43edbec711d46758 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 14 Mar 2018 16:39:15 -0700 Subject: [PATCH 087/220] Update frontend --- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements_all.txt b/requirements_all.txt index 1733f0d9a7d..b35b3d0991a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -353,7 +353,7 @@ hipnotify==1.0.8 holidays==0.9.4 # homeassistant.components.frontend -home-assistant-frontend==20180310.0 +home-assistant-frontend==20180315.0 # homeassistant.components.camera.onvif http://github.com/tgaugry/suds-passworddigest-py3/archive/86fc50e39b4d2b8997481967d6a7fe1c57118999.zip#suds-passworddigest-py3==0.1.2a diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2c785bee3af..9def3a7b301 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -78,7 +78,7 @@ hbmqtt==0.9.1 holidays==0.9.4 # homeassistant.components.frontend -home-assistant-frontend==20180310.0 +home-assistant-frontend==20180315.0 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From d348f09d3d42c6bd65727a471cf363cbca19105f Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Thu, 15 Mar 2018 02:48:21 +0100 Subject: [PATCH 088/220] HomeKit Restructure (new config options) (#12997) * Restructure * Pincode will now be autogenerated and display using a persistence notification * Added 'homekit.start' service * Added config options * Renamed files for types * Improved tests * Changes (based on feedback) * Removed CONF_PIN_CODE * Added services.yaml * Service will only be registered if auto_start=False * Bugfix names, changed default port * Generate aids with zlib.adler32 * Added entity filter, minor changes * Small changes --- homeassistant/components/homekit/__init__.py | 211 ++++++++------ .../components/homekit/accessories.py | 89 +++--- homeassistant/components/homekit/const.py | 30 +- .../components/homekit/services.yaml | 4 + .../homekit/{covers.py => type_covers.py} | 27 +- ...ty_systems.py => type_security_systems.py} | 30 +- .../homekit/{sensors.py => type_sensors.py} | 11 +- .../homekit/{switches.py => type_switches.py} | 23 +- .../{thermostats.py => type_thermostats.py} | 113 ++++---- homeassistant/components/homekit/util.py | 46 ++++ tests/components/homekit/__init__.py | 1 - tests/components/homekit/test_accessories.py | 260 +++++++++--------- .../homekit/test_get_accessories.py | 142 +++++++--- tests/components/homekit/test_homekit.py | 214 +++++++++----- tests/components/homekit/test_switches.py | 64 ----- .../{test_covers.py => test_type_covers.py} | 15 +- ...stems.py => test_type_security_systems.py} | 36 ++- .../{test_sensors.py => test_type_sensors.py} | 30 +- .../components/homekit/test_type_switches.py | 104 +++++++ ...hermostats.py => test_type_thermostats.py} | 87 +++++- tests/components/homekit/test_util.py | 83 ++++++ tests/mock/homekit.py | 133 --------- 22 files changed, 1038 insertions(+), 715 deletions(-) create mode 100644 homeassistant/components/homekit/services.yaml rename homeassistant/components/homekit/{covers.py => type_covers.py} (77%) rename homeassistant/components/homekit/{security_systems.py => type_security_systems.py} (80%) rename homeassistant/components/homekit/{sensors.py => type_sensors.py} (84%) rename homeassistant/components/homekit/{switches.py => type_switches.py} (72%) rename homeassistant/components/homekit/{thermostats.py => type_thermostats.py} (71%) create mode 100644 homeassistant/components/homekit/util.py delete mode 100644 tests/components/homekit/__init__.py delete mode 100644 tests/components/homekit/test_switches.py rename tests/components/homekit/{test_covers.py => test_type_covers.py} (87%) rename tests/components/homekit/{test_security_systems.py => test_type_security_systems.py} (72%) rename tests/components/homekit/{test_sensors.py => test_type_sensors.py} (64%) create mode 100644 tests/components/homekit/test_type_switches.py rename tests/components/homekit/{test_thermostats.py => test_type_thermostats.py} (65%) create mode 100644 tests/components/homekit/test_util.py delete mode 100644 tests/mock/homekit.py diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index ad70740536e..63013bd8fc9 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -3,154 +3,199 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/homekit/ """ -import asyncio import logging -import re +from zlib import adler32 import voluptuous as vol -from homeassistant.const import ( - ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT, CONF_PORT, - TEMP_CELSIUS, TEMP_FAHRENHEIT, - EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) from homeassistant.components.climate import ( SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW) +from homeassistant.components.cover import SUPPORT_SET_POSITION +from homeassistant.const import ( + ATTR_CODE, ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT, + CONF_PORT, TEMP_CELSIUS, TEMP_FAHRENHEIT, + EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entityfilter import FILTER_SCHEMA from homeassistant.util import get_local_ip from homeassistant.util.decorator import Registry +from .const import ( + DOMAIN, HOMEKIT_FILE, CONF_AUTO_START, CONF_ENTITY_CONFIG, CONF_FILTER, + DEFAULT_PORT, DEFAULT_AUTO_START, SERVICE_HOMEKIT_START) +from .util import ( + validate_entity_config, show_setup_message) TYPES = Registry() _LOGGER = logging.getLogger(__name__) -_RE_VALID_PINCODE = r"^(\d{3}-\d{2}-\d{3})$" - -DOMAIN = 'homekit' REQUIREMENTS = ['HAP-python==1.1.7'] -BRIDGE_NAME = 'Home Assistant' -CONF_PIN_CODE = 'pincode' - -HOMEKIT_FILE = '.homekit.state' - - -def valid_pin(value): - """Validate pin code value.""" - match = re.match(_RE_VALID_PINCODE, str(value).strip()) - if not match: - raise vol.Invalid("Pin must be in the format: '123-45-678'") - return match.group(0) - CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.All({ - vol.Optional(CONF_PORT, default=51826): vol.Coerce(int), - vol.Optional(CONF_PIN_CODE, default='123-45-678'): valid_pin, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_AUTO_START, default=DEFAULT_AUTO_START): cv.boolean, + vol.Optional(CONF_FILTER, default={}): FILTER_SCHEMA, + vol.Optional(CONF_ENTITY_CONFIG, default={}): validate_entity_config, }) }, extra=vol.ALLOW_EXTRA) -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Setup the HomeKit component.""" - _LOGGER.debug("Begin setup HomeKit") + _LOGGER.debug('Begin setup HomeKit') conf = config[DOMAIN] - port = conf.get(CONF_PORT) - pin = str.encode(conf.get(CONF_PIN_CODE)) + port = conf[CONF_PORT] + auto_start = conf[CONF_AUTO_START] + entity_filter = conf[CONF_FILTER] + entity_config = conf[CONF_ENTITY_CONFIG] - homekit = HomeKit(hass, port) - homekit.setup_bridge(pin) + homekit = HomeKit(hass, port, entity_filter, entity_config) + homekit.setup() + + if auto_start: + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, homekit.start) + return True + + def handle_homekit_service_start(service): + """Handle start HomeKit service call.""" + if homekit.started: + _LOGGER.warning('HomeKit is already running') + return + homekit.start() + + hass.services.async_register(DOMAIN, SERVICE_HOMEKIT_START, + handle_homekit_service_start) - hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_START, homekit.start_driver) return True -def import_types(): - """Import all types from files in the HomeKit directory.""" - _LOGGER.debug("Import type files.") - # pylint: disable=unused-variable - from . import ( # noqa F401 - covers, security_systems, sensors, switches, thermostats) - - -def get_accessory(hass, state): +def get_accessory(hass, state, aid, config): """Take state and return an accessory object if supported.""" + _LOGGER.debug('%s: ') + if not aid: + _LOGGER.warning('The entitiy "%s" is not supported, since it ' + 'generates an invalid aid, please change it.', + state.entity_id) + return None + if state.domain == 'sensor': unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) if unit == TEMP_CELSIUS or unit == TEMP_FAHRENHEIT: - _LOGGER.debug("Add \"%s\" as \"%s\"", + _LOGGER.debug('Add "%s" as "%s"', state.entity_id, 'TemperatureSensor') return TYPES['TemperatureSensor'](hass, state.entity_id, - state.name) + state.name, aid=aid) elif state.domain == 'cover': # Only add covers that support set_cover_position - if state.attributes.get(ATTR_SUPPORTED_FEATURES) & 4: - _LOGGER.debug("Add \"%s\" as \"%s\"", - state.entity_id, 'Window') - return TYPES['Window'](hass, state.entity_id, state.name) + features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) + if features & SUPPORT_SET_POSITION: + _LOGGER.debug('Add "%s" as "%s"', + state.entity_id, 'WindowCovering') + return TYPES['WindowCovering'](hass, state.entity_id, state.name, + aid=aid) elif state.domain == 'alarm_control_panel': - _LOGGER.debug("Add \"%s\" as \"%s\"", state.entity_id, + _LOGGER.debug('Add "%s" as "%s"', state.entity_id, 'SecuritySystem') - return TYPES['SecuritySystem'](hass, state.entity_id, state.name) + return TYPES['SecuritySystem'](hass, state.entity_id, state.name, + alarm_code=config[ATTR_CODE], aid=aid) elif state.domain == 'climate': - support_auto = False - features = state.attributes.get(ATTR_SUPPORTED_FEATURES) + features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) + support_temp_range = SUPPORT_TARGET_TEMPERATURE_LOW | \ + SUPPORT_TARGET_TEMPERATURE_HIGH # Check if climate device supports auto mode - if (features & SUPPORT_TARGET_TEMPERATURE_HIGH) \ - and (features & SUPPORT_TARGET_TEMPERATURE_LOW): - support_auto = True - _LOGGER.debug("Add \"%s\" as \"%s\"", state.entity_id, 'Thermostat') + support_auto = bool(features & support_temp_range) + + _LOGGER.debug('Add "%s" as "%s"', state.entity_id, 'Thermostat') return TYPES['Thermostat'](hass, state.entity_id, - state.name, support_auto) + state.name, support_auto, aid=aid) elif state.domain == 'switch' or state.domain == 'remote' \ or state.domain == 'input_boolean': - _LOGGER.debug("Add \"%s\" as \"%s\"", state.entity_id, 'Switch') - return TYPES['Switch'](hass, state.entity_id, state.name) + _LOGGER.debug('Add "%s" as "%s"', state.entity_id, 'Switch') + return TYPES['Switch'](hass, state.entity_id, state.name, aid=aid) + _LOGGER.warning('The entity "%s" is not supported yet', + state.entity_id) return None +def generate_aid(entity_id): + """Generate accessory aid with zlib adler32.""" + aid = adler32(entity_id.encode('utf-8')) + if aid == 0 or aid == 1: + return None + return aid + + class HomeKit(): """Class to handle all actions between HomeKit and Home Assistant.""" - def __init__(self, hass, port): + def __init__(self, hass, port, entity_filter, entity_config): """Initialize a HomeKit object.""" self._hass = hass self._port = port + self._filter = entity_filter + self._config = entity_config + self.started = False + self.bridge = None self.driver = None - def setup_bridge(self, pin): - """Setup the bridge component to track all accessories.""" - from .accessories import HomeBridge - self.bridge = HomeBridge(BRIDGE_NAME, 'homekit.bridge', pin) + def setup(self): + """Setup bridge and accessory driver.""" + from .accessories import HomeBridge, HomeDriver - def start_driver(self, event): - """Start the accessory driver.""" - from pyhap.accessory_driver import AccessoryDriver - self._hass.bus.listen_once( - EVENT_HOMEASSISTANT_STOP, self.stop_driver) + self._hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, self.stop) - import_types() - _LOGGER.debug("Start adding accessories.") - for state in self._hass.states.all(): - acc = get_accessory(self._hass, state) - if acc is not None: - self.bridge.add_accessory(acc) - - ip_address = get_local_ip() path = self._hass.config.path(HOMEKIT_FILE) - self.driver = AccessoryDriver(self.bridge, self._port, - ip_address, path) - _LOGGER.debug("Driver started") + self.bridge = HomeBridge(self._hass) + self.driver = HomeDriver(self.bridge, self._port, get_local_ip(), path) + + def add_bridge_accessory(self, state): + """Try adding accessory to bridge if configured beforehand.""" + if not state or not self._filter(state.entity_id): + return + aid = generate_aid(state.entity_id) + conf = self._config.pop(state.entity_id, {}) + acc = get_accessory(self._hass, state, aid, conf) + if acc is not None: + self.bridge.add_accessory(acc) + + def start(self, *args): + """Start the accessory driver.""" + if self.started: + return + self.started = True + + # pylint: disable=unused-variable + from . import ( # noqa F401 + type_covers, type_security_systems, type_sensors, + type_switches, type_thermostats) + + for state in self._hass.states.all(): + self.add_bridge_accessory(state) + for entity_id in self._config: + _LOGGER.warning('The entity "%s" was not setup when HomeKit ' + 'was started', entity_id) + self.bridge.set_broker(self.driver) + + if not self.bridge.paired: + show_setup_message(self.bridge, self._hass) + + _LOGGER.debug('Driver start') self.driver.start() - def stop_driver(self, event): + def stop(self, *args): """Stop the accessory driver.""" - _LOGGER.debug("Driver stop") - if self.driver is not None: + if not self.started: + return + + _LOGGER.debug('Driver stop') + if self.driver and self.driver.run_sentinel: self.driver.stop() diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index 1cd94070289..0af25bc4453 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -2,15 +2,31 @@ import logging from pyhap.accessory import Accessory, Bridge, Category +from pyhap.accessory_driver import AccessoryDriver from .const import ( - SERV_ACCESSORY_INFO, SERV_BRIDGING_STATE, MANUFACTURER, - CHAR_MODEL, CHAR_MANUFACTURER, CHAR_NAME, CHAR_SERIAL_NUMBER) - + ACCESSORY_MODEL, ACCESSORY_NAME, BRIDGE_MODEL, BRIDGE_NAME, + MANUFACTURER, SERV_ACCESSORY_INFO, SERV_BRIDGING_STATE, + CHAR_MANUFACTURER, CHAR_MODEL, CHAR_NAME, CHAR_SERIAL_NUMBER) +from .util import ( + show_setup_message, dismiss_setup_message) _LOGGER = logging.getLogger(__name__) +def add_preload_service(acc, service, chars=None): + """Define and return a service to be available for the accessory.""" + from pyhap.loader import get_serv_loader, get_char_loader + service = get_serv_loader().get(service) + if chars: + chars = chars if isinstance(chars, list) else [chars] + for char_name in chars: + char = get_char_loader().get(char_name) + service.add_characteristic(char) + acc.add_service(service) + return service + + def set_accessory_info(acc, name, model, manufacturer=MANUFACTURER, serial_number='0000'): """Set the default accessory information.""" @@ -21,36 +37,23 @@ def set_accessory_info(acc, name, model, manufacturer=MANUFACTURER, service.get_characteristic(CHAR_SERIAL_NUMBER).set_value(serial_number) -def add_preload_service(acc, service, chars=None, opt_chars=None): - """Define and return a service to be available for the accessory.""" - from pyhap.loader import get_serv_loader, get_char_loader - service = get_serv_loader().get(service) - if chars: - chars = chars if isinstance(chars, list) else [chars] - for char_name in chars: - char = get_char_loader().get(char_name) - service.add_characteristic(char) - if opt_chars: - opt_chars = opt_chars if isinstance(opt_chars, list) else [opt_chars] - for opt_char_name in opt_chars: - opt_char = get_char_loader().get(opt_char_name) - service.add_opt_characteristic(opt_char) - acc.add_service(service) - return service +def override_properties(char, properties=None, valid_values=None): + """Override characteristic property values and valid values.""" + if properties: + char.properties.update(properties) - -def override_properties(char, new_properties): - """Override characteristic property values.""" - char.properties.update(new_properties) + if valid_values: + char.properties['ValidValues'].update(valid_values) class HomeAccessory(Accessory): - """Class to extend the Accessory class.""" + """Adapter class for Accessory.""" - def __init__(self, display_name, model, category='OTHER', **kwargs): + def __init__(self, name=ACCESSORY_NAME, model=ACCESSORY_MODEL, + category='OTHER', **kwargs): """Initialize a Accessory object.""" - super().__init__(display_name, **kwargs) - set_accessory_info(self, display_name, model) + super().__init__(name, **kwargs) + set_accessory_info(self, name, model) self.category = getattr(Category, category, Category.OTHER) def _set_services(self): @@ -58,13 +61,37 @@ class HomeAccessory(Accessory): class HomeBridge(Bridge): - """Class to extend the Bridge class.""" + """Adapter class for Bridge.""" - def __init__(self, display_name, model, pincode, **kwargs): + def __init__(self, hass, name=BRIDGE_NAME, + model=BRIDGE_MODEL, **kwargs): """Initialize a Bridge object.""" - super().__init__(display_name, pincode=pincode, **kwargs) - set_accessory_info(self, display_name, model) + super().__init__(name, **kwargs) + set_accessory_info(self, name, model) + self._hass = hass def _set_services(self): add_preload_service(self, SERV_ACCESSORY_INFO) add_preload_service(self, SERV_BRIDGING_STATE) + + def setup_message(self): + """Prevent print of pyhap setup message to terminal.""" + pass + + def add_paired_client(self, client_uuid, client_public): + """Override super function to dismiss setup message if paired.""" + super().add_paired_client(client_uuid, client_public) + dismiss_setup_message(self._hass) + + def remove_paired_client(self, client_uuid): + """Override super function to show setup message if unpaired.""" + super().remove_paired_client(client_uuid) + show_setup_message(self, self._hass) + + +class HomeDriver(AccessoryDriver): + """Adapter class for AccessoryDriver.""" + + def __init__(self, *args, **kwargs): + """Initialize a AccessoryDriver object.""" + super().__init__(*args, **kwargs) diff --git a/homeassistant/components/homekit/const.py b/homeassistant/components/homekit/const.py index 73dfbf69049..d2b1caffe53 100644 --- a/homeassistant/components/homekit/const.py +++ b/homeassistant/components/homekit/const.py @@ -1,7 +1,30 @@ """Constants used be the HomeKit component.""" +# #### MISC #### +DOMAIN = 'homekit' +HOMEKIT_FILE = '.homekit.state' +HOMEKIT_NOTIFY_ID = 4663548 + +# #### CONFIG #### +CONF_AUTO_START = 'auto_start' +CONF_ENTITY_CONFIG = 'entity_config' +CONF_FILTER = 'filter' + +# #### CONFIG DEFAULTS #### +DEFAULT_AUTO_START = True +DEFAULT_PORT = 51827 + +# #### HOMEKIT COMPONENT SERVICES #### +SERVICE_HOMEKIT_START = 'start' + +# #### STRING CONSTANTS #### +ACCESSORY_MODEL = 'homekit.accessory' +ACCESSORY_NAME = 'Home Accessory' +BRIDGE_MODEL = 'homekit.bridge' +BRIDGE_NAME = 'Home Assistant' MANUFACTURER = 'HomeAssistant' -# Services + +# #### Services #### SERV_ACCESSORY_INFO = 'AccessoryInformation' SERV_BRIDGING_STATE = 'BridgingState' SERV_SECURITY_SYSTEM = 'SecuritySystem' @@ -10,7 +33,8 @@ SERV_TEMPERATURE_SENSOR = 'TemperatureSensor' SERV_THERMOSTAT = 'Thermostat' SERV_WINDOW_COVERING = 'WindowCovering' -# Characteristics + +# #### Characteristics #### CHAR_ACC_IDENTIFIER = 'AccessoryIdentifier' CHAR_CATEGORY = 'Category' CHAR_COOLING_THRESHOLD_TEMPERATURE = 'CoolingThresholdTemperature' @@ -33,5 +57,5 @@ CHAR_TARGET_SECURITY_STATE = 'SecuritySystemTargetState' CHAR_TARGET_TEMPERATURE = 'TargetTemperature' CHAR_TEMP_DISPLAY_UNITS = 'TemperatureDisplayUnits' -# Properties +# #### Properties #### PROP_CELSIUS = {'minValue': -273, 'maxValue': 999} diff --git a/homeassistant/components/homekit/services.yaml b/homeassistant/components/homekit/services.yaml new file mode 100644 index 00000000000..e30e71301b3 --- /dev/null +++ b/homeassistant/components/homekit/services.yaml @@ -0,0 +1,4 @@ +# Describes the format for available HomeKit services + +start: + description: Starts the HomeKit component driver. diff --git a/homeassistant/components/homekit/covers.py b/homeassistant/components/homekit/type_covers.py similarity index 77% rename from homeassistant/components/homekit/covers.py rename to homeassistant/components/homekit/type_covers.py index 47713f6c630..0110bff3185 100644 --- a/homeassistant/components/homekit/covers.py +++ b/homeassistant/components/homekit/type_covers.py @@ -14,16 +14,17 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -@TYPES.register('Window') -class Window(HomeAccessory): +@TYPES.register('WindowCovering') +class WindowCovering(HomeAccessory): """Generate a Window accessory for a cover entity. The cover entity must support: set_cover_position. """ - def __init__(self, hass, entity_id, display_name): + def __init__(self, hass, entity_id, display_name, *args, **kwargs): """Initialize a Window accessory object.""" - super().__init__(display_name, entity_id, 'WINDOW') + super().__init__(display_name, entity_id, 'WINDOW_COVERING', + *args, **kwargs) self._hass = hass self._entity_id = entity_id @@ -31,12 +32,12 @@ class Window(HomeAccessory): self.current_position = None self.homekit_target = None - self.serv_cover = add_preload_service(self, SERV_WINDOW_COVERING) - self.char_current_position = self.serv_cover. \ + serv_cover = add_preload_service(self, SERV_WINDOW_COVERING) + self.char_current_position = serv_cover. \ get_characteristic(CHAR_CURRENT_POSITION) - self.char_target_position = self.serv_cover. \ + self.char_target_position = serv_cover. \ get_characteristic(CHAR_TARGET_POSITION) - self.char_position_state = self.serv_cover. \ + self.char_position_state = serv_cover. \ get_characteristic(CHAR_POSITION_STATE) self.char_current_position.value = 0 self.char_target_position.value = 0 @@ -55,15 +56,14 @@ class Window(HomeAccessory): def move_cover(self, value): """Move cover to value if call came from HomeKit.""" if value != self.current_position: - _LOGGER.debug("%s: Set position to %d", self._entity_id, value) + _LOGGER.debug('%s: Set position to %d', self._entity_id, value) self.homekit_target = value if value > self.current_position: self.char_position_state.set_value(1) elif value < self.current_position: self.char_position_state.set_value(0) - self._hass.services.call( - 'cover', 'set_cover_position', - {'entity_id': self._entity_id, 'position': value}) + self._hass.components.cover.set_cover_position( + value, self._entity_id) def update_cover_position(self, entity_id=None, old_state=None, new_state=None): @@ -71,9 +71,10 @@ class Window(HomeAccessory): if new_state is None: return - current_position = new_state.attributes[ATTR_CURRENT_POSITION] + current_position = new_state.attributes.get(ATTR_CURRENT_POSITION) if current_position is None: return + self.current_position = int(current_position) self.char_current_position.set_value(self.current_position) diff --git a/homeassistant/components/homekit/security_systems.py b/homeassistant/components/homekit/type_security_systems.py similarity index 80% rename from homeassistant/components/homekit/security_systems.py rename to homeassistant/components/homekit/type_security_systems.py index 1b8f0a6820b..02742acb75d 100644 --- a/homeassistant/components/homekit/security_systems.py +++ b/homeassistant/components/homekit/type_security_systems.py @@ -28,9 +28,11 @@ STATE_TO_SERVICE = {STATE_ALARM_DISARMED: 'alarm_disarm', class SecuritySystem(HomeAccessory): """Generate an SecuritySystem accessory for an alarm control panel.""" - def __init__(self, hass, entity_id, display_name, alarm_code=None): + def __init__(self, hass, entity_id, display_name, + alarm_code, *args, **kwargs): """Initialize a SecuritySystem accessory object.""" - super().__init__(display_name, entity_id, 'ALARM_SYSTEM') + super().__init__(display_name, entity_id, 'ALARM_SYSTEM', + *args, **kwargs) self._hass = hass self._entity_id = entity_id @@ -38,11 +40,11 @@ class SecuritySystem(HomeAccessory): self.flag_target_state = False - self.service_alarm = add_preload_service(self, SERV_SECURITY_SYSTEM) - self.char_current_state = self.service_alarm. \ + serv_alarm = add_preload_service(self, SERV_SECURITY_SYSTEM) + self.char_current_state = serv_alarm. \ get_characteristic(CHAR_CURRENT_SECURITY_STATE) self.char_current_state.value = 3 - self.char_target_state = self.service_alarm. \ + self.char_target_state = serv_alarm. \ get_characteristic(CHAR_TARGET_SECURITY_STATE) self.char_target_state.value = 3 @@ -58,15 +60,13 @@ class SecuritySystem(HomeAccessory): def set_security_state(self, value): """Move security state to value if call came from HomeKit.""" - _LOGGER.debug("%s: Set security state to %d", + _LOGGER.debug('%s: Set security state to %d', self._entity_id, value) self.flag_target_state = True hass_value = HOMEKIT_TO_HASS[value] service = STATE_TO_SERVICE[hass_value] - params = {ATTR_ENTITY_ID: self._entity_id} - if self._alarm_code is not None: - params[ATTR_CODE] = self._alarm_code + params = {ATTR_ENTITY_ID: self._entity_id, ATTR_CODE: self._alarm_code} self._hass.services.call('alarm_control_panel', service, params) def update_security_state(self, entity_id=None, @@ -78,15 +78,15 @@ class SecuritySystem(HomeAccessory): hass_state = new_state.state if hass_state not in HASS_TO_HOMEKIT: return + current_security_state = HASS_TO_HOMEKIT[hass_state] - self.char_current_state.set_value(current_security_state) - _LOGGER.debug("%s: Updated current state to %s (%d)", - self._entity_id, hass_state, - current_security_state) + self.char_current_state.set_value(current_security_state, + should_callback=False) + _LOGGER.debug('%s: Updated current state to %s (%d)', + self._entity_id, hass_state, current_security_state) if not self.flag_target_state: self.char_target_state.set_value(current_security_state, should_callback=False) - elif self.char_target_state.get_value() \ - == self.char_current_state.get_value(): + if self.char_target_state.value == self.char_current_state.value: self.flag_target_state = False diff --git a/homeassistant/components/homekit/sensors.py b/homeassistant/components/homekit/type_sensors.py similarity index 84% rename from homeassistant/components/homekit/sensors.py rename to homeassistant/components/homekit/type_sensors.py index 40f97ae3ef7..286862343f4 100644 --- a/homeassistant/components/homekit/sensors.py +++ b/homeassistant/components/homekit/type_sensors.py @@ -36,16 +36,15 @@ class TemperatureSensor(HomeAccessory): Sensor entity must return temperature in °C, °F. """ - def __init__(self, hass, entity_id, display_name): + def __init__(self, hass, entity_id, display_name, *args, **kwargs): """Initialize a TemperatureSensor accessory object.""" - super().__init__(display_name, entity_id, 'SENSOR') + super().__init__(display_name, entity_id, 'SENSOR', *args, **kwargs) self._hass = hass self._entity_id = entity_id - self.serv_temp = add_preload_service(self, SERV_TEMPERATURE_SENSOR) - self.char_temp = self.serv_temp. \ - get_characteristic(CHAR_CURRENT_TEMPERATURE) + serv_temp = add_preload_service(self, SERV_TEMPERATURE_SENSOR) + self.char_temp = serv_temp.get_characteristic(CHAR_CURRENT_TEMPERATURE) override_properties(self.char_temp, PROP_CELSIUS) self.char_temp.value = 0 self.unit = None @@ -68,5 +67,5 @@ class TemperatureSensor(HomeAccessory): temperature = calc_temperature(new_state.state, unit) if temperature is not None: self.char_temp.set_value(temperature) - _LOGGER.debug("%s: Current temperature set to %d°C", + _LOGGER.debug('%s: Current temperature set to %d°C', self._entity_id, temperature) diff --git a/homeassistant/components/homekit/switches.py b/homeassistant/components/homekit/type_switches.py similarity index 72% rename from homeassistant/components/homekit/switches.py rename to homeassistant/components/homekit/type_switches.py index 876b3406d28..989bf4e19f5 100644 --- a/homeassistant/components/homekit/switches.py +++ b/homeassistant/components/homekit/type_switches.py @@ -1,7 +1,8 @@ """Class to hold all switch accessories.""" import logging -from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.const import ( + ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF, STATE_ON) from homeassistant.core import split_entity_id from homeassistant.helpers.event import async_track_state_change @@ -16,9 +17,9 @@ _LOGGER = logging.getLogger(__name__) class Switch(HomeAccessory): """Generate a Switch accessory.""" - def __init__(self, hass, entity_id, display_name): + def __init__(self, hass, entity_id, display_name, *args, **kwargs): """Initialize a Switch accessory object to represent a remote.""" - super().__init__(display_name, entity_id, 'SWITCH') + super().__init__(display_name, entity_id, 'SWITCH', *args, **kwargs) self._hass = hass self._entity_id = entity_id @@ -26,8 +27,8 @@ class Switch(HomeAccessory): self.flag_target_state = False - self.service_switch = add_preload_service(self, SERV_SWITCH) - self.char_on = self.service_switch.get_characteristic(CHAR_ON) + serv_switch = add_preload_service(self, SERV_SWITCH) + self.char_on = serv_switch.get_characteristic(CHAR_ON) self.char_on.value = False self.char_on.setter_callback = self.set_state @@ -41,10 +42,10 @@ class Switch(HomeAccessory): def set_state(self, value): """Move switch state to value if call came from HomeKit.""" - _LOGGER.debug("%s: Set switch state to %s", + _LOGGER.debug('%s: Set switch state to %s', self._entity_id, value) self.flag_target_state = True - service = 'turn_on' if value else 'turn_off' + service = SERVICE_TURN_ON if value else SERVICE_TURN_OFF self._hass.services.call(self._domain, service, {ATTR_ENTITY_ID: self._entity_id}) @@ -53,10 +54,10 @@ class Switch(HomeAccessory): if new_state is None: return - current_state = (new_state.state == 'on') + current_state = (new_state.state == STATE_ON) if not self.flag_target_state: - _LOGGER.debug("%s: Set current state to %s", + _LOGGER.debug('%s: Set current state to %s', self._entity_id, current_state) self.char_on.set_value(current_state, should_callback=False) - else: - self.flag_target_state = False + + self.flag_target_state = False diff --git a/homeassistant/components/homekit/thermostats.py b/homeassistant/components/homekit/type_thermostats.py similarity index 71% rename from homeassistant/components/homekit/thermostats.py rename to homeassistant/components/homekit/type_thermostats.py index 6d342273e8d..6e720c2214e 100644 --- a/homeassistant/components/homekit/thermostats.py +++ b/homeassistant/components/homekit/type_thermostats.py @@ -7,8 +7,7 @@ from homeassistant.components.climate import ( ATTR_OPERATION_MODE, ATTR_OPERATION_LIST, STATE_HEAT, STATE_COOL, STATE_AUTO) from homeassistant.const import ( - ATTR_ENTITY_ID, ATTR_UNIT_OF_MEASUREMENT, - TEMP_CELSIUS, TEMP_FAHRENHEIT) + ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS, TEMP_FAHRENHEIT) from homeassistant.helpers.event import async_track_state_change from . import TYPES @@ -33,9 +32,11 @@ HC_HOMEKIT_TO_HASS = {c: s for s, c in HC_HASS_TO_HOMEKIT.items()} class Thermostat(HomeAccessory): """Generate a Thermostat accessory for a climate.""" - def __init__(self, hass, entity_id, display_name, support_auto=False): + def __init__(self, hass, entity_id, display_name, + support_auto, *args, **kwargs): """Initialize a Thermostat accessory object.""" - super().__init__(display_name, entity_id, 'THERMOSTAT') + super().__init__(display_name, entity_id, 'THERMOSTAT', + *args, **kwargs) self._hass = hass self._entity_id = entity_id @@ -46,48 +47,47 @@ class Thermostat(HomeAccessory): self.coolingthresh_flag_target_state = False self.heatingthresh_flag_target_state = False - extra_chars = None # Add additional characteristics if auto mode is supported - if support_auto: - extra_chars = [CHAR_COOLING_THRESHOLD_TEMPERATURE, - CHAR_HEATING_THRESHOLD_TEMPERATURE] + extra_chars = [ + CHAR_COOLING_THRESHOLD_TEMPERATURE, + CHAR_HEATING_THRESHOLD_TEMPERATURE] if support_auto else None # Preload the thermostat service - self.service_thermostat = add_preload_service(self, SERV_THERMOSTAT, - extra_chars) + serv_thermostat = add_preload_service(self, SERV_THERMOSTAT, + extra_chars) # Current and target mode characteristics - self.char_current_heat_cool = self.service_thermostat. \ + self.char_current_heat_cool = serv_thermostat. \ get_characteristic(CHAR_CURRENT_HEATING_COOLING) self.char_current_heat_cool.value = 0 - self.char_target_heat_cool = self.service_thermostat. \ + self.char_target_heat_cool = serv_thermostat. \ get_characteristic(CHAR_TARGET_HEATING_COOLING) self.char_target_heat_cool.value = 0 self.char_target_heat_cool.setter_callback = self.set_heat_cool # Current and target temperature characteristics - self.char_current_temp = self.service_thermostat. \ + self.char_current_temp = serv_thermostat. \ get_characteristic(CHAR_CURRENT_TEMPERATURE) self.char_current_temp.value = 21.0 - self.char_target_temp = self.service_thermostat. \ + self.char_target_temp = serv_thermostat. \ get_characteristic(CHAR_TARGET_TEMPERATURE) self.char_target_temp.value = 21.0 self.char_target_temp.setter_callback = self.set_target_temperature # Display units characteristic - self.char_display_units = self.service_thermostat. \ + self.char_display_units = serv_thermostat. \ get_characteristic(CHAR_TEMP_DISPLAY_UNITS) self.char_display_units.value = 0 # If the device supports it: high and low temperature characteristics if support_auto: - self.char_cooling_thresh_temp = self.service_thermostat. \ + self.char_cooling_thresh_temp = serv_thermostat. \ get_characteristic(CHAR_COOLING_THRESHOLD_TEMPERATURE) self.char_cooling_thresh_temp.value = 23.0 self.char_cooling_thresh_temp.setter_callback = \ self.set_cooling_threshold - self.char_heating_thresh_temp = self.service_thermostat. \ + self.char_heating_thresh_temp = serv_thermostat. \ get_characteristic(CHAR_HEATING_THRESHOLD_TEMPERATURE) self.char_heating_thresh_temp.value = 19.0 self.char_heating_thresh_temp.setter_callback = \ @@ -107,47 +107,40 @@ class Thermostat(HomeAccessory): def set_heat_cool(self, value): """Move operation mode to value if call came from HomeKit.""" if value in HC_HOMEKIT_TO_HASS: - _LOGGER.debug("%s: Set heat-cool to %d", self._entity_id, value) + _LOGGER.debug('%s: Set heat-cool to %d', self._entity_id, value) self.heat_cool_flag_target_state = True hass_value = HC_HOMEKIT_TO_HASS[value] - self._hass.services.call('climate', 'set_operation_mode', - {ATTR_ENTITY_ID: self._entity_id, - ATTR_OPERATION_MODE: hass_value}) + self._hass.components.climate.set_operation_mode( + operation_mode=hass_value, entity_id=self._entity_id) def set_cooling_threshold(self, value): """Set cooling threshold temp to value if call came from HomeKit.""" - _LOGGER.debug("%s: Set cooling threshold temperature to %.2f", + _LOGGER.debug('%s: Set cooling threshold temperature to %.2f', self._entity_id, value) self.coolingthresh_flag_target_state = True - low = self.char_heating_thresh_temp.get_value() - self._hass.services.call( - 'climate', 'set_temperature', - {ATTR_ENTITY_ID: self._entity_id, - ATTR_TARGET_TEMP_HIGH: value, - ATTR_TARGET_TEMP_LOW: low}) + low = self.char_heating_thresh_temp.value + self._hass.components.climate.set_temperature( + entity_id=self._entity_id, target_temp_high=value, + target_temp_low=low) def set_heating_threshold(self, value): """Set heating threshold temp to value if call came from HomeKit.""" - _LOGGER.debug("%s: Set heating threshold temperature to %.2f", + _LOGGER.debug('%s: Set heating threshold temperature to %.2f', self._entity_id, value) self.heatingthresh_flag_target_state = True # Home assistant always wants to set low and high at the same time - high = self.char_cooling_thresh_temp.get_value() - self._hass.services.call( - 'climate', 'set_temperature', - {ATTR_ENTITY_ID: self._entity_id, - ATTR_TARGET_TEMP_LOW: value, - ATTR_TARGET_TEMP_HIGH: high}) + high = self.char_cooling_thresh_temp.value + self._hass.components.climate.set_temperature( + entity_id=self._entity_id, target_temp_high=high, + target_temp_low=value) def set_target_temperature(self, value): """Set target temperature to value if call came from HomeKit.""" - _LOGGER.debug("%s: Set target temperature to %.2f", + _LOGGER.debug('%s: Set target temperature to %.2f', self._entity_id, value) self.temperature_flag_target_state = True - self._hass.services.call( - 'climate', 'set_temperature', - {ATTR_ENTITY_ID: self._entity_id, - ATTR_TEMPERATURE: value}) + self._hass.components.climate.set_temperature( + temperature=value, entity_id=self._entity_id) def update_thermostat(self, entity_id=None, old_state=None, new_state=None): @@ -166,62 +159,58 @@ class Thermostat(HomeAccessory): if not self.temperature_flag_target_state: self.char_target_temp.set_value(target_temp, should_callback=False) - else: - self.temperature_flag_target_state = False + self.temperature_flag_target_state = False # Update cooling threshold temperature if characteristic exists - if self.char_cooling_thresh_temp is not None: + if self.char_cooling_thresh_temp: cooling_thresh = new_state.attributes.get(ATTR_TARGET_TEMP_HIGH) - if cooling_thresh is not None: + if cooling_thresh: if not self.coolingthresh_flag_target_state: self.char_cooling_thresh_temp.set_value( cooling_thresh, should_callback=False) - else: - self.coolingthresh_flag_target_state = False + self.coolingthresh_flag_target_state = False # Update heating threshold temperature if characteristic exists - if self.char_heating_thresh_temp is not None: + if self.char_heating_thresh_temp: heating_thresh = new_state.attributes.get(ATTR_TARGET_TEMP_LOW) - if heating_thresh is not None: + if heating_thresh: if not self.heatingthresh_flag_target_state: self.char_heating_thresh_temp.set_value( heating_thresh, should_callback=False) - else: - self.heatingthresh_flag_target_state = False + self.heatingthresh_flag_target_state = False # Update display units display_units = new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - if display_units is not None \ + if display_units \ and display_units in UNIT_HASS_TO_HOMEKIT: self.char_display_units.set_value( UNIT_HASS_TO_HOMEKIT[display_units]) # Update target operation mode operation_mode = new_state.attributes.get(ATTR_OPERATION_MODE) - if operation_mode is not None \ + if operation_mode \ and operation_mode in HC_HASS_TO_HOMEKIT: if not self.heat_cool_flag_target_state: self.char_target_heat_cool.set_value( HC_HASS_TO_HOMEKIT[operation_mode], should_callback=False) - else: - self.heat_cool_flag_target_state = False + self.heat_cool_flag_target_state = False # Set current operation mode based on temperatures and target mode if operation_mode == STATE_HEAT: - if current_temp < target_temp: + if isinstance(target_temp, float) and current_temp < target_temp: current_operation_mode = STATE_HEAT else: current_operation_mode = STATE_OFF elif operation_mode == STATE_COOL: - if current_temp > target_temp: + if isinstance(target_temp, float) and current_temp > target_temp: current_operation_mode = STATE_COOL else: current_operation_mode = STATE_OFF elif operation_mode == STATE_AUTO: # Check if auto is supported - if self.char_cooling_thresh_temp is not None: - lower_temp = self.char_heating_thresh_temp.get_value() - upper_temp = self.char_cooling_thresh_temp.get_value() + if self.char_cooling_thresh_temp: + lower_temp = self.char_heating_thresh_temp.value + upper_temp = self.char_cooling_thresh_temp.value if current_temp < lower_temp: current_operation_mode = STATE_HEAT elif current_temp > upper_temp: @@ -232,9 +221,11 @@ class Thermostat(HomeAccessory): # Check if heating or cooling are supported heat = STATE_HEAT in new_state.attributes[ATTR_OPERATION_LIST] cool = STATE_COOL in new_state.attributes[ATTR_OPERATION_LIST] - if current_temp < target_temp and heat: + if isinstance(target_temp, float) and \ + current_temp < target_temp and heat: current_operation_mode = STATE_HEAT - elif current_temp > target_temp and cool: + elif isinstance(target_temp, float) and \ + current_temp > target_temp and cool: current_operation_mode = STATE_COOL else: current_operation_mode = STATE_OFF diff --git a/homeassistant/components/homekit/util.py b/homeassistant/components/homekit/util.py new file mode 100644 index 00000000000..f18eb2273db --- /dev/null +++ b/homeassistant/components/homekit/util.py @@ -0,0 +1,46 @@ +"""Collection of useful functions for the HomeKit component.""" +import logging + +import voluptuous as vol + +from homeassistant.core import split_entity_id +from homeassistant.const import ( + ATTR_CODE) +import homeassistant.helpers.config_validation as cv +from .const import HOMEKIT_NOTIFY_ID + +_LOGGER = logging.getLogger(__name__) + + +def validate_entity_config(values): + """Validate config entry for CONF_ENTITY.""" + entities = {} + for key, config in values.items(): + entity = cv.entity_id(key) + params = {} + if not isinstance(config, dict): + raise vol.Invalid('The configuration for "{}" must be ' + ' an dictionary.'.format(entity)) + + domain, _ = split_entity_id(entity) + + if domain == 'alarm_control_panel': + code = config.get(ATTR_CODE) + params[ATTR_CODE] = cv.string(code) if code else None + + entities[entity] = params + return entities + + +def show_setup_message(bridge, hass): + """Display persistent notification with setup information.""" + pin = bridge.pincode.decode() + message = 'To setup Home Assistant in the Home App, enter the ' \ + 'following code:\n### {}'.format(pin) + hass.components.persistent_notification.create( + message, 'HomeKit Setup', HOMEKIT_NOTIFY_ID) + + +def dismiss_setup_message(hass): + """Dismiss persistent notification and remove QR code.""" + hass.components.persistent_notification.dismiss(HOMEKIT_NOTIFY_ID) diff --git a/tests/components/homekit/__init__.py b/tests/components/homekit/__init__.py deleted file mode 100644 index 61a60cee2ac..00000000000 --- a/tests/components/homekit/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The tests for the homekit component.""" diff --git a/tests/components/homekit/test_accessories.py b/tests/components/homekit/test_accessories.py index 6f39a8c792b..4d230b81686 100644 --- a/tests/components/homekit/test_accessories.py +++ b/tests/components/homekit/test_accessories.py @@ -2,166 +2,164 @@ This includes tests for all mock object types. """ - -from unittest.mock import patch - -# pylint: disable=unused-import -from pyhap.loader import get_serv_loader, get_char_loader # noqa F401 +import unittest +from unittest.mock import call, patch, Mock from homeassistant.components.homekit.accessories import ( - set_accessory_info, add_preload_service, override_properties, - HomeAccessory, HomeBridge) + add_preload_service, set_accessory_info, override_properties, + HomeAccessory, HomeBridge, HomeDriver) from homeassistant.components.homekit.const import ( + ACCESSORY_MODEL, ACCESSORY_NAME, BRIDGE_MODEL, BRIDGE_NAME, SERV_ACCESSORY_INFO, SERV_BRIDGING_STATE, - CHAR_MODEL, CHAR_MANUFACTURER, CHAR_NAME, CHAR_SERIAL_NUMBER) - -from tests.mock.homekit import ( - get_patch_paths, mock_preload_service, - MockTypeLoader, MockAccessory, MockService, MockChar) - -PATH_SERV = 'pyhap.loader.get_serv_loader' -PATH_CHAR = 'pyhap.loader.get_char_loader' -PATH_ACC, _ = get_patch_paths() + CHAR_MANUFACTURER, CHAR_MODEL, CHAR_NAME, CHAR_SERIAL_NUMBER) -@patch(PATH_CHAR, return_value=MockTypeLoader('char')) -@patch(PATH_SERV, return_value=MockTypeLoader('service')) -def test_add_preload_service(mock_serv, mock_char): - """Test method add_preload_service. +class TestAccessories(unittest.TestCase): + """Test pyhap adapter methods.""" - The methods 'get_serv_loader' and 'get_char_loader' are mocked. - """ - acc = MockAccessory('Accessory') - serv = add_preload_service(acc, 'TestService', - ['TestChar', 'TestChar2'], - ['TestOptChar', 'TestOptChar2']) + def test_add_preload_service(self): + """Test add_preload_service without additional characteristics.""" + acc = Mock() + serv = add_preload_service(acc, 'AirPurifier') + self.assertEqual(acc.mock_calls, [call.add_service(serv)]) + with self.assertRaises(AssertionError): + serv.get_characteristic('Name') - assert serv.display_name == 'TestService' - assert len(serv.characteristics) == 2 - assert len(serv.opt_characteristics) == 2 + # Test with typo in service name + with self.assertRaises(KeyError): + add_preload_service(Mock(), 'AirPurifierTypo') - acc.services = [] - serv = add_preload_service(acc, 'TestService') + # Test adding additional characteristic as string + serv = add_preload_service(Mock(), 'AirPurifier', 'Name') + serv.get_characteristic('Name') - assert not serv.characteristics - assert not serv.opt_characteristics + # Test adding additional characteristics as list + serv = add_preload_service(Mock(), 'AirPurifier', + ['Name', 'RotationSpeed']) + serv.get_characteristic('Name') + serv.get_characteristic('RotationSpeed') - acc.services = [] - serv = add_preload_service(acc, 'TestService', - 'TestChar', 'TestOptChar') + # Test adding additional characteristic with typo + with self.assertRaises(KeyError): + add_preload_service(Mock(), 'AirPurifier', 'NameTypo') - assert len(serv.characteristics) == 1 - assert len(serv.opt_characteristics) == 1 + def test_set_accessory_info(self): + """Test setting the basic accessory information.""" + # Test HomeAccessory + acc = HomeAccessory() + set_accessory_info(acc, 'name', 'model', 'manufacturer', '0000') - assert serv.characteristics[0].display_name == 'TestChar' - assert serv.opt_characteristics[0].display_name == 'TestOptChar' + serv = acc.get_service(SERV_ACCESSORY_INFO) + self.assertEqual(serv.get_characteristic(CHAR_NAME).value, 'name') + self.assertEqual(serv.get_characteristic(CHAR_MODEL).value, 'model') + self.assertEqual( + serv.get_characteristic(CHAR_MANUFACTURER).value, 'manufacturer') + self.assertEqual( + serv.get_characteristic(CHAR_SERIAL_NUMBER).value, '0000') + # Test HomeBridge + acc = HomeBridge(None) + set_accessory_info(acc, 'name', 'model', 'manufacturer', '0000') -def test_override_properties(): - """Test override of characteristic properties with MockChar.""" - char = MockChar('TestChar') - new_prop = {1: 'Test', 2: 'Demo'} - override_properties(char, new_prop) + serv = acc.get_service(SERV_ACCESSORY_INFO) + self.assertEqual(serv.get_characteristic(CHAR_MODEL).value, 'model') + self.assertEqual( + serv.get_characteristic(CHAR_MANUFACTURER).value, 'manufacturer') + self.assertEqual( + serv.get_characteristic(CHAR_SERIAL_NUMBER).value, '0000') - assert char.properties == new_prop + def test_override_properties(self): + """Test overriding property values.""" + serv = add_preload_service(Mock(), 'AirPurifier', 'RotationSpeed') + char_active = serv.get_characteristic('Active') + char_rotation_speed = serv.get_characteristic('RotationSpeed') -def test_set_accessory_info(): - """Test setting of basic accessory information with MockAccessory.""" - acc = MockAccessory('Accessory') - set_accessory_info(acc, 'name', 'model', 'manufacturer', '0000') + self.assertTrue( + char_active.properties['ValidValues'].get('State') is None) + self.assertEqual(char_rotation_speed.properties['maxValue'], 100) - assert len(acc.services) == 1 - serv = acc.services[0] + override_properties(char_active, valid_values={'State': 'On'}) + override_properties(char_rotation_speed, properties={'maxValue': 200}) - assert serv.display_name == SERV_ACCESSORY_INFO - assert len(serv.characteristics) == 4 - chars = serv.characteristics + self.assertFalse( + char_active.properties['ValidValues'].get('State') is None) + self.assertEqual(char_rotation_speed.properties['maxValue'], 200) - assert chars[0].display_name == CHAR_NAME - assert chars[0].value == 'name' - assert chars[1].display_name == CHAR_MODEL - assert chars[1].value == 'model' - assert chars[2].display_name == CHAR_MANUFACTURER - assert chars[2].value == 'manufacturer' - assert chars[3].display_name == CHAR_SERIAL_NUMBER - assert chars[3].value == '0000' + def test_home_accessory(self): + """Test HomeAccessory class.""" + acc = HomeAccessory() + self.assertEqual(acc.display_name, ACCESSORY_NAME) + self.assertEqual(acc.category, 1) # Category.OTHER + self.assertEqual(len(acc.services), 1) + serv = acc.services[0] # SERV_ACCESSORY_INFO + self.assertEqual( + serv.get_characteristic(CHAR_MODEL).value, ACCESSORY_MODEL) + acc = HomeAccessory('test_name', 'test_model', 'FAN', aid=2) + self.assertEqual(acc.display_name, 'test_name') + self.assertEqual(acc.category, 3) # Category.FAN + self.assertEqual(acc.aid, 2) + self.assertEqual(len(acc.services), 1) + serv = acc.services[0] # SERV_ACCESSORY_INFO + self.assertEqual( + serv.get_characteristic(CHAR_MODEL).value, 'test_model') -@patch(PATH_ACC, side_effect=mock_preload_service) -def test_home_accessory(mock_pre_serv): - """Test initializing a HomeAccessory object.""" - acc = HomeAccessory('TestAccessory', 'test.accessory', 'WINDOW') + def test_home_bridge(self): + """Test HomeBridge class.""" + bridge = HomeBridge(None) + self.assertEqual(bridge.display_name, BRIDGE_NAME) + self.assertEqual(bridge.category, 2) # Category.BRIDGE + self.assertEqual(len(bridge.services), 2) + serv = bridge.services[0] # SERV_ACCESSORY_INFO + self.assertEqual(serv.display_name, SERV_ACCESSORY_INFO) + self.assertEqual( + serv.get_characteristic(CHAR_MODEL).value, BRIDGE_MODEL) + serv = bridge.services[1] # SERV_BRIDGING_STATE + self.assertEqual(serv.display_name, SERV_BRIDGING_STATE) - assert acc.display_name == 'TestAccessory' - assert acc.category == 13 # Category.WINDOW - assert len(acc.services) == 1 + bridge = HomeBridge('hass', 'test_name', 'test_model') + self.assertEqual(bridge.display_name, 'test_name') + self.assertEqual(len(bridge.services), 2) + serv = bridge.services[0] # SERV_ACCESSORY_INFO + self.assertEqual( + serv.get_characteristic(CHAR_MODEL).value, 'test_model') - serv = acc.services[0] - assert serv.display_name == SERV_ACCESSORY_INFO - char_model = serv.get_characteristic(CHAR_MODEL) - assert char_model.get_value() == 'test.accessory' + # setup_message + bridge.setup_message() + # add_paired_client + with patch('pyhap.accessory.Accessory.add_paired_client') \ + as mock_add_paired_client, \ + patch('homeassistant.components.homekit.accessories.' + 'dismiss_setup_message') as mock_dissmiss_msg: + bridge.add_paired_client('client_uuid', 'client_public') -@patch(PATH_ACC, side_effect=mock_preload_service) -def test_home_bridge(mock_pre_serv): - """Test initializing a HomeBridge object.""" - bridge = HomeBridge('TestBridge', 'test.bridge', b'123-45-678') + self.assertEqual(mock_add_paired_client.call_args, + call('client_uuid', 'client_public')) + self.assertEqual(mock_dissmiss_msg.call_args, call('hass')) - assert bridge.display_name == 'TestBridge' - assert bridge.pincode == b'123-45-678' - assert len(bridge.services) == 2 + # remove_paired_client + with patch('pyhap.accessory.Accessory.remove_paired_client') \ + as mock_remove_paired_client, \ + patch('homeassistant.components.homekit.accessories.' + 'show_setup_message') as mock_show_msg: + bridge.remove_paired_client('client_uuid') - assert bridge.services[0].display_name == SERV_ACCESSORY_INFO - assert bridge.services[1].display_name == SERV_BRIDGING_STATE + self.assertEqual( + mock_remove_paired_client.call_args, call('client_uuid')) + self.assertEqual(mock_show_msg.call_args, call(bridge, 'hass')) - char_model = bridge.services[0].get_characteristic(CHAR_MODEL) - assert char_model.get_value() == 'test.bridge' + def test_home_driver(self): + """Test HomeDriver class.""" + bridge = HomeBridge(None) + ip_adress = '127.0.0.1' + port = 51826 + path = '.homekit.state' + with patch('pyhap.accessory_driver.AccessoryDriver.__init__') \ + as mock_driver: + HomeDriver(bridge, ip_adress, port, path) -def test_mock_accessory(): - """Test attributes and functions of a MockAccessory.""" - acc = MockAccessory('TestAcc') - serv = MockService('TestServ') - acc.add_service(serv) - - assert acc.display_name == 'TestAcc' - assert len(acc.services) == 1 - - assert acc.get_service('TestServ') == serv - assert acc.get_service('NewServ').display_name == 'NewServ' - assert len(acc.services) == 2 - - -def test_mock_service(): - """Test attributes and functions of a MockService.""" - serv = MockService('TestServ') - char = MockChar('TestChar') - opt_char = MockChar('TestOptChar') - serv.add_characteristic(char) - serv.add_opt_characteristic(opt_char) - - assert serv.display_name == 'TestServ' - assert len(serv.characteristics) == 1 - assert len(serv.opt_characteristics) == 1 - - assert serv.get_characteristic('TestChar') == char - assert serv.get_characteristic('TestOptChar') == opt_char - assert serv.get_characteristic('NewChar').display_name == 'NewChar' - assert len(serv.characteristics) == 2 - - -def test_mock_char(): - """Test attributes and functions of a MockChar.""" - def callback_method(value): - """Provide a callback options for 'set_value' method.""" - assert value == 'With callback' - - char = MockChar('TestChar') - char.set_value('Value') - - assert char.display_name == 'TestChar' - assert char.get_value() == 'Value' - - char.setter_callback = callback_method - char.set_value('With callback') + self.assertEqual( + mock_driver.call_args, call(bridge, ip_adress, port, path)) diff --git a/tests/components/homekit/test_get_accessories.py b/tests/components/homekit/test_get_accessories.py index 6e49674a7b9..6e1c67cf282 100644 --- a/tests/components/homekit/test_get_accessories.py +++ b/tests/components/homekit/test_get_accessories.py @@ -1,57 +1,113 @@ """Package to test the get_accessory method.""" -from unittest.mock import patch, MagicMock +import logging +import unittest +from unittest.mock import patch, Mock from homeassistant.core import State -from homeassistant.components.homekit import ( - TYPES, get_accessory, import_types) +from homeassistant.components.climate import ( + SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW) +from homeassistant.components.homekit import get_accessory, TYPES from homeassistant.const import ( - ATTR_UNIT_OF_MEASUREMENT, ATTR_SUPPORTED_FEATURES, - TEMP_CELSIUS, TEMP_FAHRENHEIT, STATE_UNKNOWN) + ATTR_CODE, ATTR_UNIT_OF_MEASUREMENT, ATTR_SUPPORTED_FEATURES, + TEMP_CELSIUS, TEMP_FAHRENHEIT) + +_LOGGER = logging.getLogger(__name__) + +CONFIG = {} -def test_import_types(): - """Test if all type files are imported correctly.""" - try: - import_types() - assert True - # pylint: disable=broad-except - except Exception: - assert False - - -def test_component_not_supported(): +def test_get_accessory_invalid(caplog): """Test with unsupported component.""" - state = State('demo.unsupported', STATE_UNKNOWN) + assert get_accessory(None, State('test.unsupported', 'on'), 2, None) \ + is None + assert caplog.records[1].levelname == 'WARNING' - assert True if get_accessory(None, state) is None else False + assert get_accessory(None, State('test.test', 'on'), None, None) \ + is None + assert caplog.records[3].levelname == 'WARNING' -def test_sensor_temperature_celsius(): - """Test temperature sensor with Celsius as unit.""" - mock_type = MagicMock() - with patch.dict(TYPES, {'TemperatureSensor': mock_type}): - state = State('sensor.temperature', '23', - {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) - get_accessory(None, state) - assert len(mock_type.mock_calls) == 1 +class TestGetAccessories(unittest.TestCase): + """Methods to test the get_accessory method.""" + def setUp(self): + """Setup Mock type.""" + self.mock_type = Mock() -# pylint: disable=invalid-name -def test_sensor_temperature_fahrenheit(): - """Test temperature sensor with Fahrenheit as unit.""" - mock_type = MagicMock() - with patch.dict(TYPES, {'TemperatureSensor': mock_type}): - state = State('sensor.temperature', '74', - {ATTR_UNIT_OF_MEASUREMENT: TEMP_FAHRENHEIT}) - get_accessory(None, state) - assert len(mock_type.mock_calls) == 1 + def tearDown(self): + """Test if mock type was called.""" + self.assertTrue(self.mock_type.called) + def test_sensor_temperature_celsius(self): + """Test temperature sensor with Celsius as unit.""" + with patch.dict(TYPES, {'TemperatureSensor': self.mock_type}): + state = State('sensor.temperature', '23', + {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) + get_accessory(None, state, 2, {}) -def test_cover_set_position(): - """Test cover with support for set_cover_position.""" - mock_type = MagicMock() - with patch.dict(TYPES, {'Window': mock_type}): - state = State('cover.set_position', 'open', - {ATTR_SUPPORTED_FEATURES: 4}) - get_accessory(None, state) - assert len(mock_type.mock_calls) == 1 + # pylint: disable=invalid-name + def test_sensor_temperature_fahrenheit(self): + """Test temperature sensor with Fahrenheit as unit.""" + with patch.dict(TYPES, {'TemperatureSensor': self.mock_type}): + state = State('sensor.temperature', '74', + {ATTR_UNIT_OF_MEASUREMENT: TEMP_FAHRENHEIT}) + get_accessory(None, state, 2, {}) + + def test_cover_set_position(self): + """Test cover with support for set_cover_position.""" + with patch.dict(TYPES, {'WindowCovering': self.mock_type}): + state = State('cover.set_position', 'open', + {ATTR_SUPPORTED_FEATURES: 4}) + get_accessory(None, state, 2, {}) + + def test_alarm_control_panel(self): + """Test alarm control panel.""" + config = {ATTR_CODE: '1234'} + with patch.dict(TYPES, {'SecuritySystem': self.mock_type}): + state = State('alarm_control_panel.test', 'armed') + get_accessory(None, state, 2, config) + + # pylint: disable=unsubscriptable-object + self.assertEqual( + self.mock_type.call_args[1].get('alarm_code'), '1234') + + def test_climate(self): + """Test climate devices.""" + with patch.dict(TYPES, {'Thermostat': self.mock_type}): + state = State('climate.test', 'auto') + get_accessory(None, state, 2, {}) + + # pylint: disable=unsubscriptable-object + self.assertEqual( + self.mock_type.call_args[0][-1], False) # support_auto + + def test_climate_support_auto(self): + """Test climate devices with support for auto mode.""" + with patch.dict(TYPES, {'Thermostat': self.mock_type}): + state = State('climate.test', 'auto', { + ATTR_SUPPORTED_FEATURES: + SUPPORT_TARGET_TEMPERATURE_LOW | + SUPPORT_TARGET_TEMPERATURE_HIGH}) + get_accessory(None, state, 2, {}) + + # pylint: disable=unsubscriptable-object + self.assertEqual( + self.mock_type.call_args[0][-1], True) # support_auto + + def test_switch(self): + """Test switch.""" + with patch.dict(TYPES, {'Switch': self.mock_type}): + state = State('switch.test', 'on') + get_accessory(None, state, 2, {}) + + def test_remote(self): + """Test remote.""" + with patch.dict(TYPES, {'Switch': self.mock_type}): + state = State('remote.test', 'on') + get_accessory(None, state, 2, {}) + + def test_input_boolean(self): + """Test input_boolean.""" + with patch.dict(TYPES, {'Switch': self.mock_type}): + state = State('input_boolean.test', 'on') + get_accessory(None, state, 2, {}) diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py index 58c197e69ec..c6d79545487 100644 --- a/tests/components/homekit/test_homekit.py +++ b/tests/components/homekit/test_homekit.py @@ -1,33 +1,22 @@ """Tests for the HomeKit component.""" - import unittest -from unittest.mock import call, patch, ANY - -import voluptuous as vol - -# pylint: disable=unused-import -from pyhap.accessory_driver import AccessoryDriver # noqa F401 +from unittest.mock import call, patch, ANY, Mock from homeassistant import setup -from homeassistant.core import Event -from homeassistant.components.homekit import ( - CONF_PIN_CODE, HOMEKIT_FILE, HomeKit, valid_pin) +from homeassistant.core import State +from homeassistant.components.homekit import HomeKit, generate_aid +from homeassistant.components.homekit.accessories import HomeBridge +from homeassistant.components.homekit.const import ( + DOMAIN, HOMEKIT_FILE, CONF_AUTO_START, + DEFAULT_PORT, SERVICE_HOMEKIT_START) +from homeassistant.helpers.entityfilter import generate_filter from homeassistant.const import ( CONF_PORT, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) from tests.common import get_test_home_assistant -from tests.mock.homekit import get_patch_paths, PATH_HOMEKIT -PATH_ACC, _ = get_patch_paths() IP_ADDRESS = '127.0.0.1' - -CONFIG_MIN = {'homekit': {}} -CONFIG = { - 'homekit': { - CONF_PORT: 11111, - CONF_PIN_CODE: '987-65-432', - } -} +PATH_HOMEKIT = 'homeassistant.components.homekit' class TestHomeKit(unittest.TestCase): @@ -41,75 +30,162 @@ class TestHomeKit(unittest.TestCase): """Stop down everything that was started.""" self.hass.stop() - def test_validate_pincode(self): - """Test async_setup with invalid config option.""" - schema = vol.Schema(valid_pin) + def test_generate_aid(self): + """Test generate aid method.""" + aid = generate_aid('demo.entity') + self.assertIsInstance(aid, int) + self.assertTrue(aid >= 2 and aid <= 18446744073709551615) - for value in ('', '123-456-78', 'a23-45-678', '12345678', 1234): - with self.assertRaises(vol.MultipleInvalid): - schema(value) - - for value in ('123-45-678', '234-56-789'): - self.assertTrue(schema(value)) + with patch(PATH_HOMEKIT + '.adler32') as mock_adler32: + mock_adler32.side_effect = [0, 1] + self.assertIsNone(generate_aid('demo.entity')) @patch(PATH_HOMEKIT + '.HomeKit') def test_setup_min(self, mock_homekit): - """Test async_setup with minimal config option.""" + """Test async_setup with min config options.""" self.assertTrue(setup.setup_component( - self.hass, 'homekit', CONFIG_MIN)) + self.hass, DOMAIN, {DOMAIN: {}})) - self.assertEqual(mock_homekit.mock_calls, - [call(self.hass, 51826), - call().setup_bridge(b'123-45-678')]) + self.assertEqual(mock_homekit.mock_calls, [ + call(self.hass, DEFAULT_PORT, ANY, {}), + call().setup()]) + + # Test auto start enabled mock_homekit.reset_mock() + self.hass.bus.fire(EVENT_HOMEASSISTANT_START) + self.hass.block_till_done() + + self.assertEqual(mock_homekit.mock_calls, [call().start(ANY)]) + + @patch(PATH_HOMEKIT + '.HomeKit') + def test_setup_auto_start_disabled(self, mock_homekit): + """Test async_setup with auto start disabled and test service calls.""" + mock_homekit.return_value = homekit = Mock() + + config = {DOMAIN: {CONF_AUTO_START: False, CONF_PORT: 11111}} + self.assertTrue(setup.setup_component( + self.hass, DOMAIN, config)) self.hass.bus.fire(EVENT_HOMEASSISTANT_START) self.hass.block_till_done() - self.assertEqual(mock_homekit.mock_calls, - [call().start_driver(ANY)]) + self.assertEqual(mock_homekit.mock_calls, [ + call(self.hass, 11111, ANY, {}), + call().setup()]) - @patch(PATH_HOMEKIT + '.HomeKit') - def test_setup_parameters(self, mock_homekit): - """Test async_setup with full config option.""" - self.assertTrue(setup.setup_component( - self.hass, 'homekit', CONFIG)) + # Test start call with driver stopped. + homekit.reset_mock() + homekit.configure_mock(**{'started': False}) - self.assertEqual(mock_homekit.mock_calls, - [call(self.hass, 11111), - call().setup_bridge(b'987-65-432')]) + self.hass.services.call('homekit', 'start') + self.assertEqual(homekit.mock_calls, [call.start()]) - @patch('pyhap.accessory_driver.AccessoryDriver') - def test_homekit_class(self, mock_acc_driver): - """Test interaction between the HomeKit class and pyhap.""" - with patch(PATH_HOMEKIT + '.accessories.HomeBridge') as mock_bridge: - homekit = HomeKit(self.hass, 51826) - homekit.setup_bridge(b'123-45-678') + # Test start call with driver started. + homekit.reset_mock() + homekit.configure_mock(**{'started': True}) - mock_bridge.reset_mock() - self.hass.states.set('demo.demo1', 'on') - self.hass.states.set('demo.demo2', 'off') + self.hass.services.call(DOMAIN, SERVICE_HOMEKIT_START) + self.assertEqual(homekit.mock_calls, []) - with patch(PATH_HOMEKIT + '.get_accessory') as mock_get_acc, \ - patch(PATH_HOMEKIT + '.import_types') as mock_import_types, \ + def test_homekit_setup(self): + """Test setup of bridge and driver.""" + homekit = HomeKit(self.hass, DEFAULT_PORT, {}, {}) + + with patch(PATH_HOMEKIT + '.accessories.HomeDriver') as mock_driver, \ patch('homeassistant.util.get_local_ip') as mock_ip: - mock_get_acc.side_effect = ['TempSensor', 'Window'] mock_ip.return_value = IP_ADDRESS - homekit.start_driver(Event(EVENT_HOMEASSISTANT_START)) + homekit.setup() path = self.hass.config.path(HOMEKIT_FILE) + self.assertTrue(isinstance(homekit.bridge, HomeBridge)) + self.assertEqual(mock_driver.mock_calls, [ + call(homekit.bridge, DEFAULT_PORT, IP_ADDRESS, path)]) - self.assertEqual(mock_import_types.call_count, 1) - self.assertEqual(mock_get_acc.call_count, 2) - self.assertEqual(mock_bridge.mock_calls, - [call().add_accessory('TempSensor'), - call().add_accessory('Window')]) - self.assertEqual(mock_acc_driver.mock_calls, - [call(homekit.bridge, 51826, IP_ADDRESS, path), - call().start()]) - mock_acc_driver.reset_mock() + # Test if stop listener is setup + self.assertEqual( + self.hass.bus.listeners.get(EVENT_HOMEASSISTANT_STOP), 1) - self.hass.bus.fire(EVENT_HOMEASSISTANT_STOP) - self.hass.block_till_done() + def test_homekit_add_accessory(self): + """Add accessory if config exists and get_acc returns an accessory.""" + homekit = HomeKit(self.hass, None, lambda entity_id: True, {}) + homekit.bridge = HomeBridge(self.hass) - self.assertEqual(mock_acc_driver.mock_calls, [call().stop()]) + with patch(PATH_HOMEKIT + '.accessories.HomeBridge.add_accessory') \ + as mock_add_acc, \ + patch(PATH_HOMEKIT + '.get_accessory') as mock_get_acc: + mock_get_acc.side_effect = [None, 'acc', None] + homekit.add_bridge_accessory(State('light.demo', 'on')) + self.assertEqual(mock_get_acc.call_args, + call(self.hass, ANY, 363398124, {})) + self.assertFalse(mock_add_acc.called) + homekit.add_bridge_accessory(State('demo.test', 'on')) + self.assertEqual(mock_get_acc.call_args, + call(self.hass, ANY, 294192020, {})) + self.assertTrue(mock_add_acc.called) + homekit.add_bridge_accessory(State('demo.test_2', 'on')) + self.assertEqual(mock_get_acc.call_args, + call(self.hass, ANY, 429982757, {})) + self.assertEqual(mock_add_acc.mock_calls, [call('acc')]) + + def test_homekit_entity_filter(self): + """Test the entity filter.""" + entity_filter = generate_filter(['cover'], ['demo.test'], [], []) + homekit = HomeKit(self.hass, None, entity_filter, {}) + + with patch(PATH_HOMEKIT + '.get_accessory') as mock_get_acc: + mock_get_acc.return_value = None + + homekit.add_bridge_accessory(State('cover.test', 'open')) + self.assertTrue(mock_get_acc.called) + mock_get_acc.reset_mock() + + homekit.add_bridge_accessory(State('demo.test', 'on')) + self.assertTrue(mock_get_acc.called) + mock_get_acc.reset_mock() + + homekit.add_bridge_accessory(State('light.demo', 'light')) + self.assertFalse(mock_get_acc.called) + + @patch(PATH_HOMEKIT + '.show_setup_message') + @patch(PATH_HOMEKIT + '.HomeKit.add_bridge_accessory') + def test_homekit_start(self, mock_add_bridge_acc, mock_show_setup_msg): + """Test HomeKit start method.""" + homekit = HomeKit(self.hass, None, {}, {'cover.demo': {}}) + homekit.bridge = HomeBridge(self.hass) + homekit.driver = Mock() + + self.hass.states.set('light.demo', 'on') + state = self.hass.states.all()[0] + + homekit.start() + + self.assertEqual(mock_add_bridge_acc.mock_calls, [call(state)]) + self.assertEqual(mock_show_setup_msg.mock_calls, [ + call(homekit.bridge, self.hass)]) + self.assertEqual(homekit.driver.mock_calls, [call.start()]) + self.assertTrue(homekit.started) + + # Test start() if already started + homekit.driver.reset_mock() + homekit.start() + self.assertEqual(homekit.driver.mock_calls, []) + + def test_homekit_stop(self): + """Test HomeKit stop method.""" + homekit = HomeKit(None, None, None, None) + homekit.driver = Mock() + + # Test if started = False + homekit.stop() + self.assertFalse(homekit.driver.stop.called) + + # Test if driver not started + homekit.started = True + homekit.driver.configure_mock(**{'run_sentinel': None}) + homekit.stop() + self.assertFalse(homekit.driver.stop.called) + + # Test if driver is started + homekit.driver.configure_mock(**{'run_sentinel': 'sentinel'}) + homekit.stop() + self.assertTrue(homekit.driver.stop.called) diff --git a/tests/components/homekit/test_switches.py b/tests/components/homekit/test_switches.py deleted file mode 100644 index d9f2d6c1d1a..00000000000 --- a/tests/components/homekit/test_switches.py +++ /dev/null @@ -1,64 +0,0 @@ -"""Test different accessory types: Switches.""" -import unittest -from unittest.mock import patch - -from homeassistant.core import callback -from homeassistant.components.homekit.switches import Switch -from homeassistant.const import ATTR_SERVICE, EVENT_CALL_SERVICE - -from tests.common import get_test_home_assistant -from tests.mock.homekit import get_patch_paths, mock_preload_service - -PATH_ACC, PATH_FILE = get_patch_paths('switches') - - -class TestHomekitSwitches(unittest.TestCase): - """Test class for all accessory types regarding switches.""" - - def setUp(self): - """Setup things to be run when tests are started.""" - self.hass = get_test_home_assistant() - self.events = [] - - @callback - def record_event(event): - """Track called event.""" - self.events.append(event) - - self.hass.bus.listen(EVENT_CALL_SERVICE, record_event) - - def tearDown(self): - """Stop down everything that was started.""" - self.hass.stop() - - def test_switch_set_state(self): - """Test if accessory and HA are updated accordingly.""" - switch = 'switch.testswitch' - - with patch(PATH_ACC, side_effect=mock_preload_service): - with patch(PATH_FILE, side_effect=mock_preload_service): - acc = Switch(self.hass, switch, 'Switch') - acc.run() - - self.assertEqual(acc.char_on.value, False) - - self.hass.states.set(switch, 'on') - self.hass.block_till_done() - self.assertEqual(acc.char_on.value, True) - - self.hass.states.set(switch, 'off') - self.hass.block_till_done() - self.assertEqual(acc.char_on.value, False) - - # Set from HomeKit - acc.char_on.set_value(True) - self.hass.block_till_done() - self.assertEqual( - self.events[0].data[ATTR_SERVICE], 'turn_on') - self.assertEqual(acc.char_on.value, True) - - acc.char_on.set_value(False) - self.hass.block_till_done() - self.assertEqual( - self.events[1].data[ATTR_SERVICE], 'turn_off') - self.assertEqual(acc.char_on.value, False) diff --git a/tests/components/homekit/test_covers.py b/tests/components/homekit/test_type_covers.py similarity index 87% rename from tests/components/homekit/test_covers.py rename to tests/components/homekit/test_type_covers.py index fe0ede5d8fb..45631a76c98 100644 --- a/tests/components/homekit/test_covers.py +++ b/tests/components/homekit/test_type_covers.py @@ -1,19 +1,15 @@ """Test different accessory types: Covers.""" import unittest -from unittest.mock import patch from homeassistant.core import callback from homeassistant.components.cover import ( ATTR_POSITION, ATTR_CURRENT_POSITION) -from homeassistant.components.homekit.covers import Window +from homeassistant.components.homekit.type_covers import WindowCovering from homeassistant.const import ( STATE_UNKNOWN, STATE_OPEN, ATTR_SERVICE, ATTR_SERVICE_DATA, EVENT_CALL_SERVICE) from tests.common import get_test_home_assistant -from tests.mock.homekit import get_patch_paths, mock_preload_service - -PATH_ACC, PATH_FILE = get_patch_paths('covers') class TestHomekitSensors(unittest.TestCase): @@ -39,10 +35,11 @@ class TestHomekitSensors(unittest.TestCase): """Test if accessory and HA are updated accordingly.""" window_cover = 'cover.window' - with patch(PATH_ACC, side_effect=mock_preload_service): - with patch(PATH_FILE, side_effect=mock_preload_service): - acc = Window(self.hass, window_cover, 'Cover') - acc.run() + acc = WindowCovering(self.hass, window_cover, 'Cover', aid=2) + acc.run() + + self.assertEqual(acc.aid, 2) + self.assertEqual(acc.category, 14) # WindowCovering self.assertEqual(acc.char_current_position.value, 0) self.assertEqual(acc.char_target_position.value, 0) diff --git a/tests/components/homekit/test_security_systems.py b/tests/components/homekit/test_type_security_systems.py similarity index 72% rename from tests/components/homekit/test_security_systems.py rename to tests/components/homekit/test_type_security_systems.py index 4753e86c084..4d61fc4a44c 100644 --- a/tests/components/homekit/test_security_systems.py +++ b/tests/components/homekit/test_type_security_systems.py @@ -1,18 +1,15 @@ """Test different accessory types: Security Systems.""" import unittest -from unittest.mock import patch from homeassistant.core import callback -from homeassistant.components.homekit.security_systems import SecuritySystem +from homeassistant.components.homekit.type_security_systems import ( + SecuritySystem) from homeassistant.const import ( - ATTR_SERVICE, EVENT_CALL_SERVICE, + ATTR_CODE, ATTR_SERVICE, ATTR_SERVICE_DATA, EVENT_CALL_SERVICE, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, - STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED) + STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED, STATE_UNKNOWN) from tests.common import get_test_home_assistant -from tests.mock.homekit import get_patch_paths, mock_preload_service - -PATH_ACC, PATH_FILE = get_patch_paths('security_systems') class TestHomekitSecuritySystems(unittest.TestCase): @@ -36,12 +33,14 @@ class TestHomekitSecuritySystems(unittest.TestCase): def test_switch_set_state(self): """Test if accessory and HA are updated accordingly.""" - acp = 'alarm_control_panel.testsecurity' + acp = 'alarm_control_panel.test' - with patch(PATH_ACC, side_effect=mock_preload_service): - with patch(PATH_FILE, side_effect=mock_preload_service): - acc = SecuritySystem(self.hass, acp, 'SecuritySystem') - acc.run() + acc = SecuritySystem(self.hass, acp, 'SecuritySystem', + alarm_code='1234', aid=2) + acc.run() + + self.assertEqual(acc.aid, 2) + self.assertEqual(acc.category, 11) # AlarmSystem self.assertEqual(acc.char_current_state.value, 3) self.assertEqual(acc.char_target_state.value, 3) @@ -66,27 +65,40 @@ class TestHomekitSecuritySystems(unittest.TestCase): self.assertEqual(acc.char_target_state.value, 3) self.assertEqual(acc.char_current_state.value, 3) + self.hass.states.set(acp, STATE_UNKNOWN) + self.hass.block_till_done() + self.assertEqual(acc.char_target_state.value, 3) + self.assertEqual(acc.char_current_state.value, 3) + # Set from HomeKit acc.char_target_state.set_value(0) self.hass.block_till_done() self.assertEqual( self.events[0].data[ATTR_SERVICE], 'alarm_arm_home') + self.assertEqual( + self.events[0].data[ATTR_SERVICE_DATA][ATTR_CODE], '1234') self.assertEqual(acc.char_target_state.value, 0) acc.char_target_state.set_value(1) self.hass.block_till_done() self.assertEqual( self.events[1].data[ATTR_SERVICE], 'alarm_arm_away') + self.assertEqual( + self.events[0].data[ATTR_SERVICE_DATA][ATTR_CODE], '1234') self.assertEqual(acc.char_target_state.value, 1) acc.char_target_state.set_value(2) self.hass.block_till_done() self.assertEqual( self.events[2].data[ATTR_SERVICE], 'alarm_arm_night') + self.assertEqual( + self.events[0].data[ATTR_SERVICE_DATA][ATTR_CODE], '1234') self.assertEqual(acc.char_target_state.value, 2) acc.char_target_state.set_value(3) self.hass.block_till_done() self.assertEqual( self.events[3].data[ATTR_SERVICE], 'alarm_disarm') + self.assertEqual( + self.events[0].data[ATTR_SERVICE_DATA][ATTR_CODE], '1234') self.assertEqual(acc.char_target_state.value, 3) diff --git a/tests/components/homekit/test_sensors.py b/tests/components/homekit/test_type_sensors.py similarity index 64% rename from tests/components/homekit/test_sensors.py rename to tests/components/homekit/test_type_sensors.py index 4698c363503..f9a14f6b8cf 100644 --- a/tests/components/homekit/test_sensors.py +++ b/tests/components/homekit/test_type_sensors.py @@ -1,17 +1,13 @@ """Test different accessory types: Sensors.""" import unittest -from unittest.mock import patch from homeassistant.components.homekit.const import PROP_CELSIUS -from homeassistant.components.homekit.sensors import ( +from homeassistant.components.homekit.type_sensors import ( TemperatureSensor, calc_temperature) from homeassistant.const import ( - ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS, TEMP_FAHRENHEIT, STATE_UNKNOWN) + ATTR_UNIT_OF_MEASUREMENT, STATE_UNKNOWN, TEMP_CELSIUS, TEMP_FAHRENHEIT) from tests.common import get_test_home_assistant -from tests.mock.homekit import get_patch_paths, mock_preload_service - -PATH_ACC, PATH_FILE = get_patch_paths('sensors') def test_calc_temperature(): @@ -32,7 +28,6 @@ class TestHomekitSensors(unittest.TestCase): def setUp(self): """Setup things to be run when tests are started.""" self.hass = get_test_home_assistant() - get_patch_paths('sensors') def tearDown(self): """Stop down everything that was started.""" @@ -40,27 +35,28 @@ class TestHomekitSensors(unittest.TestCase): def test_temperature(self): """Test if accessory is updated after state change.""" - temperature_sensor = 'sensor.temperature' + entity_id = 'sensor.temperature' - with patch(PATH_ACC, side_effect=mock_preload_service): - with patch(PATH_FILE, side_effect=mock_preload_service): - acc = TemperatureSensor(self.hass, temperature_sensor, - 'Temperature') - acc.run() + acc = TemperatureSensor(self.hass, entity_id, 'Temperature', aid=2) + acc.run() + + self.assertEqual(acc.aid, 2) + self.assertEqual(acc.category, 10) # Sensor self.assertEqual(acc.char_temp.value, 0.0) - self.assertEqual(acc.char_temp.properties, PROP_CELSIUS) + for key, value in PROP_CELSIUS.items(): + self.assertEqual(acc.char_temp.properties[key], value) - self.hass.states.set(temperature_sensor, STATE_UNKNOWN, + self.hass.states.set(entity_id, STATE_UNKNOWN, {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) self.hass.block_till_done() - self.hass.states.set(temperature_sensor, '20', + self.hass.states.set(entity_id, '20', {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) self.hass.block_till_done() self.assertEqual(acc.char_temp.value, 20) - self.hass.states.set(temperature_sensor, '75.2', + self.hass.states.set(entity_id, '75.2', {ATTR_UNIT_OF_MEASUREMENT: TEMP_FAHRENHEIT}) self.hass.block_till_done() self.assertEqual(acc.char_temp.value, 24) diff --git a/tests/components/homekit/test_type_switches.py b/tests/components/homekit/test_type_switches.py new file mode 100644 index 00000000000..21d7583152e --- /dev/null +++ b/tests/components/homekit/test_type_switches.py @@ -0,0 +1,104 @@ +"""Test different accessory types: Switches.""" +import unittest + +from homeassistant.core import callback, split_entity_id +from homeassistant.components.homekit.type_switches import Switch +from homeassistant.const import ( + ATTR_DOMAIN, ATTR_SERVICE, EVENT_CALL_SERVICE, + SERVICE_TURN_ON, SERVICE_TURN_OFF, STATE_ON, STATE_OFF) + +from tests.common import get_test_home_assistant + + +class TestHomekitSwitches(unittest.TestCase): + """Test class for all accessory types regarding switches.""" + + def setUp(self): + """Setup things to be run when tests are started.""" + self.hass = get_test_home_assistant() + self.events = [] + + @callback + def record_event(event): + """Track called event.""" + self.events.append(event) + + self.hass.bus.listen(EVENT_CALL_SERVICE, record_event) + + def tearDown(self): + """Stop down everything that was started.""" + self.hass.stop() + + def test_switch_set_state(self): + """Test if accessory and HA are updated accordingly.""" + entity_id = 'switch.test' + domain = split_entity_id(entity_id)[0] + + acc = Switch(self.hass, entity_id, 'Switch', aid=2) + acc.run() + + self.assertEqual(acc.aid, 2) + self.assertEqual(acc.category, 8) # Switch + + self.assertEqual(acc.char_on.value, False) + + self.hass.states.set(entity_id, STATE_ON) + self.hass.block_till_done() + self.assertEqual(acc.char_on.value, True) + + self.hass.states.set(entity_id, STATE_OFF) + self.hass.block_till_done() + self.assertEqual(acc.char_on.value, False) + + # Set from HomeKit + acc.char_on.set_value(True) + self.hass.block_till_done() + self.assertEqual( + self.events[0].data[ATTR_DOMAIN], domain) + self.assertEqual( + self.events[0].data[ATTR_SERVICE], SERVICE_TURN_ON) + + acc.char_on.set_value(False) + self.hass.block_till_done() + self.assertEqual( + self.events[1].data[ATTR_DOMAIN], domain) + self.assertEqual( + self.events[1].data[ATTR_SERVICE], SERVICE_TURN_OFF) + + def test_remote_set_state(self): + """Test service call for remote as domain.""" + entity_id = 'remote.test' + domain = split_entity_id(entity_id)[0] + + acc = Switch(self.hass, entity_id, 'Switch', aid=2) + acc.run() + + self.assertEqual(acc.char_on.value, False) + + # Set from HomeKit + acc.char_on.set_value(True) + self.hass.block_till_done() + self.assertEqual( + self.events[0].data[ATTR_DOMAIN], domain) + self.assertEqual( + self.events[0].data[ATTR_SERVICE], SERVICE_TURN_ON) + self.assertEqual(acc.char_on.value, True) + + def test_input_boolean_set_state(self): + """Test service call for remote as domain.""" + entity_id = 'input_boolean.test' + domain = split_entity_id(entity_id)[0] + + acc = Switch(self.hass, entity_id, 'Switch', aid=2) + acc.run() + + self.assertEqual(acc.char_on.value, False) + + # Set from HomeKit + acc.char_on.set_value(True) + self.hass.block_till_done() + self.assertEqual( + self.events[0].data[ATTR_DOMAIN], domain) + self.assertEqual( + self.events[0].data[ATTR_SERVICE], SERVICE_TURN_ON) + self.assertEqual(acc.char_on.value, True) diff --git a/tests/components/homekit/test_thermostats.py b/tests/components/homekit/test_type_thermostats.py similarity index 65% rename from tests/components/homekit/test_thermostats.py rename to tests/components/homekit/test_type_thermostats.py index fabffe881bb..6505bf72efb 100644 --- a/tests/components/homekit/test_thermostats.py +++ b/tests/components/homekit/test_type_thermostats.py @@ -1,21 +1,18 @@ """Test different accessory types: Thermostats.""" import unittest -from unittest.mock import patch from homeassistant.core import callback from homeassistant.components.climate import ( ATTR_CURRENT_TEMPERATURE, ATTR_TEMPERATURE, - ATTR_TARGET_TEMP_LOW, ATTR_TARGET_TEMP_HIGH, - ATTR_OPERATION_MODE, STATE_HEAT, STATE_AUTO) -from homeassistant.components.homekit.thermostats import Thermostat, STATE_OFF + ATTR_TARGET_TEMP_LOW, ATTR_TARGET_TEMP_HIGH, ATTR_OPERATION_MODE, + ATTR_OPERATION_LIST, STATE_COOL, STATE_HEAT, STATE_AUTO) +from homeassistant.components.homekit.type_thermostats import ( + Thermostat, STATE_OFF) from homeassistant.const import ( ATTR_SERVICE, EVENT_CALL_SERVICE, ATTR_SERVICE_DATA, ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS) from tests.common import get_test_home_assistant -from tests.mock.homekit import get_patch_paths, mock_preload_service - -PATH_ACC, PATH_FILE = get_patch_paths('thermostats') class TestHomekitThermostats(unittest.TestCase): @@ -39,12 +36,13 @@ class TestHomekitThermostats(unittest.TestCase): def test_default_thermostat(self): """Test if accessory and HA are updated accordingly.""" - climate = 'climate.testclimate' + climate = 'climate.test' - with patch(PATH_ACC, side_effect=mock_preload_service): - with patch(PATH_FILE, side_effect=mock_preload_service): - acc = Thermostat(self.hass, climate, 'Climate', False) - acc.run() + acc = Thermostat(self.hass, climate, 'Climate', False, aid=2) + acc.run() + + self.assertEqual(acc.aid, 2) + self.assertEqual(acc.category, 9) # Thermostat self.assertEqual(acc.char_current_heat_cool.value, 0) self.assertEqual(acc.char_target_heat_cool.value, 0) @@ -78,6 +76,30 @@ class TestHomekitThermostats(unittest.TestCase): self.assertEqual(acc.char_current_temp.value, 23.0) self.assertEqual(acc.char_display_units.value, 0) + self.hass.states.set(climate, STATE_COOL, + {ATTR_OPERATION_MODE: STATE_COOL, + ATTR_TEMPERATURE: 20.0, + ATTR_CURRENT_TEMPERATURE: 25.0, + ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) + self.hass.block_till_done() + self.assertEqual(acc.char_target_temp.value, 20.0) + self.assertEqual(acc.char_current_heat_cool.value, 2) + self.assertEqual(acc.char_target_heat_cool.value, 2) + self.assertEqual(acc.char_current_temp.value, 25.0) + self.assertEqual(acc.char_display_units.value, 0) + + self.hass.states.set(climate, STATE_COOL, + {ATTR_OPERATION_MODE: STATE_COOL, + ATTR_TEMPERATURE: 20.0, + ATTR_CURRENT_TEMPERATURE: 19.0, + ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) + self.hass.block_till_done() + self.assertEqual(acc.char_target_temp.value, 20.0) + self.assertEqual(acc.char_current_heat_cool.value, 0) + self.assertEqual(acc.char_target_heat_cool.value, 2) + self.assertEqual(acc.char_current_temp.value, 19.0) + self.assertEqual(acc.char_display_units.value, 0) + self.hass.states.set(climate, STATE_OFF, {ATTR_OPERATION_MODE: STATE_OFF, ATTR_TEMPERATURE: 22.0, @@ -90,6 +112,45 @@ class TestHomekitThermostats(unittest.TestCase): self.assertEqual(acc.char_current_temp.value, 18.0) self.assertEqual(acc.char_display_units.value, 0) + self.hass.states.set(climate, STATE_AUTO, + {ATTR_OPERATION_MODE: STATE_AUTO, + ATTR_OPERATION_LIST: [STATE_HEAT, STATE_COOL], + ATTR_TEMPERATURE: 22.0, + ATTR_CURRENT_TEMPERATURE: 18.0, + ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) + self.hass.block_till_done() + self.assertEqual(acc.char_target_temp.value, 22.0) + self.assertEqual(acc.char_current_heat_cool.value, 1) + self.assertEqual(acc.char_target_heat_cool.value, 3) + self.assertEqual(acc.char_current_temp.value, 18.0) + self.assertEqual(acc.char_display_units.value, 0) + + self.hass.states.set(climate, STATE_AUTO, + {ATTR_OPERATION_MODE: STATE_AUTO, + ATTR_OPERATION_LIST: [STATE_HEAT, STATE_COOL], + ATTR_TEMPERATURE: 22.0, + ATTR_CURRENT_TEMPERATURE: 25.0, + ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) + self.hass.block_till_done() + self.assertEqual(acc.char_target_temp.value, 22.0) + self.assertEqual(acc.char_current_heat_cool.value, 2) + self.assertEqual(acc.char_target_heat_cool.value, 3) + self.assertEqual(acc.char_current_temp.value, 25.0) + self.assertEqual(acc.char_display_units.value, 0) + + self.hass.states.set(climate, STATE_AUTO, + {ATTR_OPERATION_MODE: STATE_AUTO, + ATTR_OPERATION_LIST: [STATE_HEAT, STATE_COOL], + ATTR_TEMPERATURE: 22.0, + ATTR_CURRENT_TEMPERATURE: 22.0, + ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) + self.hass.block_till_done() + self.assertEqual(acc.char_target_temp.value, 22.0) + self.assertEqual(acc.char_current_heat_cool.value, 0) + self.assertEqual(acc.char_target_heat_cool.value, 3) + self.assertEqual(acc.char_current_temp.value, 22.0) + self.assertEqual(acc.char_display_units.value, 0) + # Set from HomeKit acc.char_target_temp.set_value(19.0) self.hass.block_till_done() @@ -110,7 +171,7 @@ class TestHomekitThermostats(unittest.TestCase): def test_auto_thermostat(self): """Test if accessory and HA are updated accordingly.""" - climate = 'climate.testclimate' + climate = 'climate.test' acc = Thermostat(self.hass, climate, 'Climate', True) acc.run() diff --git a/tests/components/homekit/test_util.py b/tests/components/homekit/test_util.py new file mode 100644 index 00000000000..f95db9a4a13 --- /dev/null +++ b/tests/components/homekit/test_util.py @@ -0,0 +1,83 @@ +"""Test HomeKit util module.""" +import unittest + +import voluptuous as vol + +from homeassistant.core import callback +from homeassistant.components.homekit.accessories import HomeBridge +from homeassistant.components.homekit.const import HOMEKIT_NOTIFY_ID +from homeassistant.components.homekit.util import ( + show_setup_message, dismiss_setup_message, ATTR_CODE) +from homeassistant.components.homekit.util import validate_entity_config \ + as vec +from homeassistant.components.persistent_notification import ( + SERVICE_CREATE, SERVICE_DISMISS, ATTR_NOTIFICATION_ID) +from homeassistant.const import ( + EVENT_CALL_SERVICE, ATTR_DOMAIN, ATTR_SERVICE, ATTR_SERVICE_DATA) + +from tests.common import get_test_home_assistant + + +class TestUtil(unittest.TestCase): + """Test all HomeKit util methods.""" + + def setUp(self): + """Setup things to be run when tests are started.""" + self.hass = get_test_home_assistant() + self.events = [] + + @callback + def record_event(event): + """Track called event.""" + self.events.append(event) + + self.hass.bus.listen(EVENT_CALL_SERVICE, record_event) + + def tearDown(self): + """Stop down everything that was started.""" + self.hass.stop() + + def test_validate_entity_config(self): + """Test validate entities.""" + configs = [{'invalid_entity_id': {}}, {'demo.test': 1}, + {'demo.test': 'test'}, {'demo.test': [1, 2]}, + {'demo.test': None}] + + for conf in configs: + with self.assertRaises(vol.Invalid): + vec(conf) + + self.assertEqual(vec({}), {}) + self.assertEqual( + vec({'alarm_control_panel.demo': {ATTR_CODE: '1234'}}), + {'alarm_control_panel.demo': {ATTR_CODE: '1234'}}) + + def test_show_setup_msg(self): + """Test show setup message as persistence notification.""" + bridge = HomeBridge(self.hass) + + show_setup_message(bridge, self.hass) + self.hass.block_till_done() + + data = self.events[0].data + self.assertEqual( + data.get(ATTR_DOMAIN, None), 'persistent_notification') + self.assertEqual(data.get(ATTR_SERVICE, None), SERVICE_CREATE) + self.assertNotEqual(data.get(ATTR_SERVICE_DATA, None), None) + self.assertEqual( + data[ATTR_SERVICE_DATA].get(ATTR_NOTIFICATION_ID, None), + HOMEKIT_NOTIFY_ID) + + def test_dismiss_setup_msg(self): + """Test dismiss setup message.""" + dismiss_setup_message(self.hass) + self.hass.block_till_done() + + data = self.events[0].data + self.assertEqual( + data.get(ATTR_DOMAIN, None), 'persistent_notification') + self.assertEqual(data.get(ATTR_SERVICE, None), SERVICE_DISMISS) + self.assertNotEqual(data.get(ATTR_SERVICE_DATA, None), None) + self.assertEqual( + data[ATTR_SERVICE_DATA].get(ATTR_NOTIFICATION_ID, None), + HOMEKIT_NOTIFY_ID) diff --git a/tests/mock/homekit.py b/tests/mock/homekit.py deleted file mode 100644 index 2872fa59f19..00000000000 --- a/tests/mock/homekit.py +++ /dev/null @@ -1,133 +0,0 @@ -"""Basic mock functions and objects related to the HomeKit component.""" -PATH_HOMEKIT = 'homeassistant.components.homekit' - - -def get_patch_paths(name=None): - """Return paths to mock 'add_preload_service'.""" - path_acc = PATH_HOMEKIT + '.accessories.add_preload_service' - path_file = PATH_HOMEKIT + '.' + str(name) + '.add_preload_service' - return (path_acc, path_file) - - -def mock_preload_service(acc, service, chars=None, opt_chars=None): - """Mock alternative for function 'add_preload_service'.""" - service = MockService(service) - if chars: - chars = chars if isinstance(chars, list) else [chars] - for char_name in chars: - service.add_characteristic(char_name) - if opt_chars: - opt_chars = opt_chars if isinstance(opt_chars, list) else [opt_chars] - for opt_char_name in opt_chars: - service.add_characteristic(opt_char_name) - acc.add_service(service) - return service - - -class MockAccessory(): - """Define all attributes and methods for a MockAccessory.""" - - def __init__(self, name): - """Initialize a MockAccessory object.""" - self.display_name = name - self.services = [] - - def __repr__(self): - """Return a representation of a MockAccessory. Use for debugging.""" - serv_list = [serv.display_name for serv in self.services] - return "".format( - self.display_name, serv_list) - - def add_service(self, service): - """Add service to list of services.""" - self.services.append(service) - - def get_service(self, name): - """Retrieve service from service list or return new MockService.""" - for serv in self.services: - if serv.display_name == name: - return serv - serv = MockService(name) - self.add_service(serv) - return serv - - -class MockService(): - """Define all attributes and methods for a MockService.""" - - def __init__(self, name): - """Initialize a MockService object.""" - self.characteristics = [] - self.opt_characteristics = [] - self.display_name = name - - def __repr__(self): - """Return a representation of a MockService. Use for debugging.""" - char_list = [char.display_name for char in self.characteristics] - opt_char_list = [ - char.display_name for char in self.opt_characteristics] - return "".format( - self.display_name, char_list, opt_char_list) - - def add_characteristic(self, char): - """Add characteristic to char list.""" - self.characteristics.append(char) - - def add_opt_characteristic(self, char): - """Add characteristic to opt_char list.""" - self.opt_characteristics.append(char) - - def get_characteristic(self, name): - """Get char for char lists or return new MockChar.""" - for char in self.characteristics: - if char.display_name == name: - return char - for char in self.opt_characteristics: - if char.display_name == name: - return char - char = MockChar(name) - self.add_characteristic(char) - return char - - -class MockChar(): - """Define all attributes and methods for a MockChar.""" - - def __init__(self, name): - """Initialize a MockChar object.""" - self.display_name = name - self.properties = {} - self.value = None - self.type_id = None - self.setter_callback = None - - def __repr__(self): - """Return a representation of a MockChar. Use for debugging.""" - return "".format( - self.display_name, self.value) - - def set_value(self, value, should_notify=True, should_callback=True): - """Set value of char.""" - self.value = value - if self.setter_callback is not None and should_callback: - # pylint: disable=not-callable - self.setter_callback(value) - - def get_value(self): - """Get char value.""" - return self.value - - -class MockTypeLoader(): - """Define all attributes and methods for a MockTypeLoader.""" - - def __init__(self, class_type): - """Initialize a MockTypeLoader object.""" - self.class_type = class_type - - def get(self, name): - """Return a MockService or MockChar object.""" - if self.class_type == 'service': - return MockService(name) - elif self.class_type == 'char': - return MockChar(name) From 76874e1cbc17bd0f13f76ce524f5a2aacf832786 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 14 Mar 2018 19:47:31 -0700 Subject: [PATCH 089/220] Update translations --- .../.translations/de.json | 24 +++++++++++++++++ .../.translations/fi.json | 11 ++++++++ .../.translations/ko.json | 24 +++++++++++++++++ .../.translations/nl.json | 23 ++++++++++++++++ .../.translations/no.json | 24 +++++++++++++++++ .../.translations/pl.json | 24 +++++++++++++++++ .../.translations/ro.json | 15 +++++++++++ .../.translations/sl.json | 24 +++++++++++++++++ .../.translations/zh-Hans.json | 24 +++++++++++++++++ .../components/hue/.translations/de.json | 26 +++++++++++++++++++ .../components/hue/.translations/en.json | 2 +- .../components/hue/.translations/ko.json | 26 +++++++++++++++++++ .../components/hue/.translations/nl.json | 26 +++++++++++++++++++ .../components/hue/.translations/no.json | 26 +++++++++++++++++++ .../components/hue/.translations/pl.json | 26 +++++++++++++++++++ .../components/hue/.translations/ro.json | 18 +++++++++++++ .../components/hue/.translations/sl.json | 26 +++++++++++++++++++ .../components/hue/.translations/zh-Hans.json | 26 +++++++++++++++++++ .../sensor/.translations/season.cs.json | 8 ++++++ .../sensor/.translations/season.cy.json | 8 ++++++ .../sensor/.translations/season.de.json | 8 ++++++ .../sensor/.translations/season.es.json | 8 ++++++ .../sensor/.translations/season.fi.json | 8 ++++++ .../sensor/.translations/season.ja.json | 8 ++++++ .../sensor/.translations/season.ko.json | 8 ++++++ .../sensor/.translations/season.nl.json | 8 ++++++ .../sensor/.translations/season.no.json | 8 ++++++ .../sensor/.translations/season.pl.json | 8 ++++++ .../sensor/.translations/season.pt.json | 8 ++++++ .../sensor/.translations/season.ro.json | 8 ++++++ .../sensor/.translations/season.sl.json | 8 ++++++ .../sensor/.translations/season.sv.json | 8 ++++++ .../sensor/.translations/season.th.json | 8 ++++++ .../sensor/.translations/season.zh-Hans.json | 8 ++++++ .../sensor/.translations/season.zh-Hant.json | 8 ++++++ 35 files changed, 530 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/config_entry_example/.translations/de.json create mode 100644 homeassistant/components/config_entry_example/.translations/fi.json create mode 100644 homeassistant/components/config_entry_example/.translations/ko.json create mode 100644 homeassistant/components/config_entry_example/.translations/nl.json create mode 100644 homeassistant/components/config_entry_example/.translations/no.json create mode 100644 homeassistant/components/config_entry_example/.translations/pl.json create mode 100644 homeassistant/components/config_entry_example/.translations/ro.json create mode 100644 homeassistant/components/config_entry_example/.translations/sl.json create mode 100644 homeassistant/components/config_entry_example/.translations/zh-Hans.json create mode 100644 homeassistant/components/hue/.translations/de.json create mode 100644 homeassistant/components/hue/.translations/ko.json create mode 100644 homeassistant/components/hue/.translations/nl.json create mode 100644 homeassistant/components/hue/.translations/no.json create mode 100644 homeassistant/components/hue/.translations/pl.json create mode 100644 homeassistant/components/hue/.translations/ro.json create mode 100644 homeassistant/components/hue/.translations/sl.json create mode 100644 homeassistant/components/hue/.translations/zh-Hans.json create mode 100644 homeassistant/components/sensor/.translations/season.cs.json create mode 100644 homeassistant/components/sensor/.translations/season.cy.json create mode 100644 homeassistant/components/sensor/.translations/season.de.json create mode 100644 homeassistant/components/sensor/.translations/season.es.json create mode 100644 homeassistant/components/sensor/.translations/season.fi.json create mode 100644 homeassistant/components/sensor/.translations/season.ja.json create mode 100644 homeassistant/components/sensor/.translations/season.ko.json create mode 100644 homeassistant/components/sensor/.translations/season.nl.json create mode 100644 homeassistant/components/sensor/.translations/season.no.json create mode 100644 homeassistant/components/sensor/.translations/season.pl.json create mode 100644 homeassistant/components/sensor/.translations/season.pt.json create mode 100644 homeassistant/components/sensor/.translations/season.ro.json create mode 100644 homeassistant/components/sensor/.translations/season.sl.json create mode 100644 homeassistant/components/sensor/.translations/season.sv.json create mode 100644 homeassistant/components/sensor/.translations/season.th.json create mode 100644 homeassistant/components/sensor/.translations/season.zh-Hans.json create mode 100644 homeassistant/components/sensor/.translations/season.zh-Hant.json diff --git a/homeassistant/components/config_entry_example/.translations/de.json b/homeassistant/components/config_entry_example/.translations/de.json new file mode 100644 index 00000000000..75b88f2f822 --- /dev/null +++ b/homeassistant/components/config_entry_example/.translations/de.json @@ -0,0 +1,24 @@ +{ + "config": { + "error": { + "invalid_object_id": "Ung\u00fcltige Objekt-ID" + }, + "step": { + "init": { + "data": { + "object_id": "Objekt-ID" + }, + "description": "Bitte gib eine Objekt_ID f\u00fcr das Test-Entity ein.", + "title": "W\u00e4hle eine Objekt-ID" + }, + "name": { + "data": { + "name": "Name" + }, + "description": "Bitte gib einen Namen f\u00fcr das Test-Entity ein", + "title": "Name des Test-Entity" + } + }, + "title": "Beispiel Konfig-Eintrag" + } +} \ No newline at end of file diff --git a/homeassistant/components/config_entry_example/.translations/fi.json b/homeassistant/components/config_entry_example/.translations/fi.json new file mode 100644 index 00000000000..054a6f372bc --- /dev/null +++ b/homeassistant/components/config_entry_example/.translations/fi.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "name": { + "data": { + "name": "Nimi" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/config_entry_example/.translations/ko.json b/homeassistant/components/config_entry_example/.translations/ko.json new file mode 100644 index 00000000000..f12e3fc52f1 --- /dev/null +++ b/homeassistant/components/config_entry_example/.translations/ko.json @@ -0,0 +1,24 @@ +{ + "config": { + "error": { + "invalid_object_id": "\uc624\ube0c\uc81d\ud2b8 ID\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "step": { + "init": { + "data": { + "object_id": "\uc624\ube0c\uc81d\ud2b8 ID" + }, + "description": "\ud14c\uc2a4\ud2b8 \uad6c\uc131\uc694\uc18c\uc758 \uc624\ube0c\uc81d\ud2b8 ID \ub97c \uc785\ub825\ud558\uc138\uc694", + "title": "\uc624\ube0c\uc81d\ud2b8 ID \uc120\ud0dd" + }, + "name": { + "data": { + "name": "\uc774\ub984" + }, + "description": "\ud14c\uc2a4\ud2b8 \uad6c\uc131\uc694\uc18c\uc758 \uc774\ub984\uc744 \uc785\ub825\ud558\uc138\uc694.", + "title": "\uad6c\uc131\uc694\uc18c \uc774\ub984" + } + }, + "title": "\uc785\ub825 \uc608\uc81c \uad6c\uc131" + } +} \ No newline at end of file diff --git a/homeassistant/components/config_entry_example/.translations/nl.json b/homeassistant/components/config_entry_example/.translations/nl.json new file mode 100644 index 00000000000..10469dd0804 --- /dev/null +++ b/homeassistant/components/config_entry_example/.translations/nl.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "invalid_object_id": "Ongeldig object ID" + }, + "step": { + "init": { + "data": { + "object_id": "Object ID" + }, + "description": "Voer een object_id in voor het testen van de entiteit.", + "title": "Kies object id" + }, + "name": { + "data": { + "name": "Naam" + }, + "description": "Voer een naam in voor het testen van de entiteit.", + "title": "Naam van de entiteit" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/config_entry_example/.translations/no.json b/homeassistant/components/config_entry_example/.translations/no.json new file mode 100644 index 00000000000..380c539f8af --- /dev/null +++ b/homeassistant/components/config_entry_example/.translations/no.json @@ -0,0 +1,24 @@ +{ + "config": { + "error": { + "invalid_object_id": "Ugyldig objekt ID" + }, + "step": { + "init": { + "data": { + "object_id": "Objekt ID" + }, + "description": "Vennligst skriv inn en object_id for testenheten.", + "title": "Velg objekt ID" + }, + "name": { + "data": { + "name": "Navn" + }, + "description": "Vennligst skriv inn et navn for testenheten.", + "title": "Navn p\u00e5 enheten" + } + }, + "title": "Konfigureringseksempel" + } +} \ No newline at end of file diff --git a/homeassistant/components/config_entry_example/.translations/pl.json b/homeassistant/components/config_entry_example/.translations/pl.json new file mode 100644 index 00000000000..35cca168249 --- /dev/null +++ b/homeassistant/components/config_entry_example/.translations/pl.json @@ -0,0 +1,24 @@ +{ + "config": { + "error": { + "invalid_object_id": "Nieprawid\u0142owy identyfikator obiektu" + }, + "step": { + "init": { + "data": { + "object_id": "Identyfikator obiektu" + }, + "description": "Prosz\u0119 wprowadzi\u0107 identyfikator obiektu (object_id) dla jednostki testowej.", + "title": "Wybierz identyfikator obiektu" + }, + "name": { + "data": { + "name": "Nazwa" + }, + "description": "Prosz\u0119 wprowadzi\u0107 nazw\u0119 dla jednostki testowej.", + "title": "Nazwa jednostki" + } + }, + "title": "Przyk\u0142ad wpisu do konfiguracji" + } +} \ No newline at end of file diff --git a/homeassistant/components/config_entry_example/.translations/ro.json b/homeassistant/components/config_entry_example/.translations/ro.json new file mode 100644 index 00000000000..1a4cdd6bbb7 --- /dev/null +++ b/homeassistant/components/config_entry_example/.translations/ro.json @@ -0,0 +1,15 @@ +{ + "config": { + "step": { + "init": { + "description": "Introduce\u021bi un obiect_id pentru entitatea testat\u0103.", + "title": "Alege\u021bi id-ul obiectului" + }, + "name": { + "data": { + "name": "Nume" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/config_entry_example/.translations/sl.json b/homeassistant/components/config_entry_example/.translations/sl.json new file mode 100644 index 00000000000..11d2d3f5e80 --- /dev/null +++ b/homeassistant/components/config_entry_example/.translations/sl.json @@ -0,0 +1,24 @@ +{ + "config": { + "error": { + "invalid_object_id": "Neveljaven ID objekta" + }, + "step": { + "init": { + "data": { + "object_id": "ID objekta" + }, + "description": "Prosimo, vnesite Id_objekta za testni subjekt.", + "title": "Izberite ID objekta" + }, + "name": { + "data": { + "name": "Ime" + }, + "description": "Vnesite ime za testni subjekt.", + "title": "Ime subjekta" + } + }, + "title": "Primer nastavitve" + } +} \ No newline at end of file diff --git a/homeassistant/components/config_entry_example/.translations/zh-Hans.json b/homeassistant/components/config_entry_example/.translations/zh-Hans.json new file mode 100644 index 00000000000..ee10e6d7b48 --- /dev/null +++ b/homeassistant/components/config_entry_example/.translations/zh-Hans.json @@ -0,0 +1,24 @@ +{ + "config": { + "error": { + "invalid_object_id": "\u65e0\u6548\u7684\u5bf9\u8c61 ID" + }, + "step": { + "init": { + "data": { + "object_id": "\u5bf9\u8c61 ID" + }, + "description": "\u8bf7\u4e3a\u6d4b\u8bd5\u8bbe\u5907\u8f93\u5165\u5bf9\u8c61 ID", + "title": "\u8bf7\u9009\u62e9\u5bf9\u8c61 ID" + }, + "name": { + "data": { + "name": "\u540d\u79f0" + }, + "description": "\u8bf7\u4e3a\u6d4b\u8bd5\u8bbe\u5907\u8f93\u5165\u540d\u79f0", + "title": "\u8bbe\u5907\u540d\u79f0" + } + }, + "title": "\u6837\u4f8b\u914d\u7f6e\u6761\u76ee" + } +} \ No newline at end of file diff --git a/homeassistant/components/hue/.translations/de.json b/homeassistant/components/hue/.translations/de.json new file mode 100644 index 00000000000..b7094d91528 --- /dev/null +++ b/homeassistant/components/hue/.translations/de.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "all_configured": "Alle Philips Hue Bridges sind bereits konfiguriert", + "discover_timeout": "Nicht in der Lage Hue Bridges zu entdecken", + "no_bridges": "Philips Hue Bridges entdeckt" + }, + "error": { + "linking": "Unbekannte Link-Fehler aufgetreten.", + "register_failed": "Registrieren fehlgeschlagen, bitte versuche es erneut" + }, + "step": { + "init": { + "data": { + "host": "Host" + }, + "title": "W\u00e4hle eine Hue Bridge" + }, + "link": { + "description": "Dr\u00fccke den Knopf auf der Bridge, um Philips Hue mit Home Assistant zu registrieren.\n\n![Position des Buttons auf der Bridge](/static/images/config_philips_hue.jpg)", + "title": "Hub verbinden" + } + }, + "title": "Philips Hue Bridge" + } +} \ No newline at end of file diff --git a/homeassistant/components/hue/.translations/en.json b/homeassistant/components/hue/.translations/en.json index ee2e01fdb17..cbf63301da2 100644 --- a/homeassistant/components/hue/.translations/en.json +++ b/homeassistant/components/hue/.translations/en.json @@ -23,4 +23,4 @@ }, "title": "Philips Hue Bridge" } -} +} \ No newline at end of file diff --git a/homeassistant/components/hue/.translations/ko.json b/homeassistant/components/hue/.translations/ko.json new file mode 100644 index 00000000000..226ae8ba1f6 --- /dev/null +++ b/homeassistant/components/hue/.translations/ko.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "all_configured": "\ubaa8\ub4e0 \ud544\ub9bd\uc2a4 Hue \ube0c\ub9bf\uc9c0\uac00 \uc774\ubbf8 \uc124\uc815\ub41c \uc0c1\ud0dc\uc785\ub2c8\ub2e4", + "discover_timeout": "Hue \ube0c\ub9bf\uc9c0\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", + "no_bridges": "\ubc1c\uacac\ub41c \ud544\ub9bd\uc2a4 Hue \ube0c\ub9bf\uc9c0\uac00 \uc5c6\uc2b5\ub2c8\ub2e4" + }, + "error": { + "linking": "\uc54c \uc218\uc5c6\ub294 \uc5f0\uacb0 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.", + "register_failed": "\ub4f1\ub85d\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\ubcf4\uc138\uc694" + }, + "step": { + "init": { + "data": { + "host": "\ud638\uc2a4\ud2b8" + }, + "title": "Hue \ube0c\ub9bf\uc9c0 \uc120\ud0dd" + }, + "link": { + "description": "\ube0c\ub9bf\uc9c0\uc758 \ubc84\ud2bc\uc744 \ub20c\ub7ec \ud544\ub9bd\uc2a4 Hue\ub97c Home Assistant\uc5d0 \ub4f1\ub85d\ud558\uc138\uc694.\n\n![\ube0c\ub9bf\uc9c0 \ubc84\ud2bc \uc704\uce58](/static/images/config_philips_hue.jpg)", + "title": "\ud5c8\ube0c \uc5f0\uacb0" + } + }, + "title": "\ud544\ub9bd\uc2a4 Hue \ube0c\ub9bf\uc9c0" + } +} \ No newline at end of file diff --git a/homeassistant/components/hue/.translations/nl.json b/homeassistant/components/hue/.translations/nl.json new file mode 100644 index 00000000000..750ae39db12 --- /dev/null +++ b/homeassistant/components/hue/.translations/nl.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "all_configured": "Alle Philips Hue bridges zijn al geconfigureerd", + "discover_timeout": "Hue bridges kunnen niet worden gevonden", + "no_bridges": "Geen Philips Hue bridges ontdekt" + }, + "error": { + "linking": "Er is een onbekende verbindingsfout opgetreden.", + "register_failed": "Registratie is mislukt, probeer het opnieuw" + }, + "step": { + "init": { + "data": { + "host": "Host" + }, + "title": "Kies Hue bridge" + }, + "link": { + "description": "Druk op de knop van de bridge om Philips Hue te registreren met de Home Assistant. ![Locatie van de knop op bridge] (/static/images/config_philips_hue.jpg)", + "title": "Link Hub" + } + }, + "title": "Philips Hue Bridge" + } +} \ No newline at end of file diff --git a/homeassistant/components/hue/.translations/no.json b/homeassistant/components/hue/.translations/no.json new file mode 100644 index 00000000000..604475d2ff2 --- /dev/null +++ b/homeassistant/components/hue/.translations/no.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "all_configured": "Alle Philips Hue Bridger er allerede konfigurert", + "discover_timeout": "Kunne ikke oppdage Hue Bridger", + "no_bridges": "Ingen Philips Hue Bridger oppdaget" + }, + "error": { + "linking": "Ukjent koblingsfeil oppstod.", + "register_failed": "Registrering feilet, vennligst pr\u00f8v igjen" + }, + "step": { + "init": { + "data": { + "host": "Vert" + }, + "title": "Velg Hue Bridge" + }, + "link": { + "description": "Trykk p\u00e5 knappen p\u00e5 Bridgen for \u00e5 registrere Philips Hue med Home Assistant. \n\n ![Knappens plassering p\u00e5 Bridgen](/static/images/config_philips_hue.jpg)", + "title": "Link Hub" + } + }, + "title": "Philips Hue Bridge" + } +} \ No newline at end of file diff --git a/homeassistant/components/hue/.translations/pl.json b/homeassistant/components/hue/.translations/pl.json new file mode 100644 index 00000000000..cdd26a5b4b2 --- /dev/null +++ b/homeassistant/components/hue/.translations/pl.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "all_configured": "Wszystkie mostki Hue s\u0105 ju\u017c skonfigurowane", + "discover_timeout": "Nie mo\u017cna wykry\u0107 \u017cadnych mostk\u00f3w Hue", + "no_bridges": "Nie wykryto \u017cadnych mostk\u00f3w Hue" + }, + "error": { + "linking": "Wyst\u0105pi\u0142 nieznany b\u0142\u0105d w trakcie \u0142\u0105czenia.", + "register_failed": "Nie uda\u0142o si\u0119 zarejestrowa\u0107. Prosz\u0119 spr\u00f3bowa\u0107 ponownie." + }, + "step": { + "init": { + "data": { + "host": "Host" + }, + "title": "Wybierz mostek Hue" + }, + "link": { + "description": "Naci\u015bnij przycisk na mostku, aby zarejestrowa\u0107 Philips Hue z Home Assistant. ", + "title": "Hub Link" + } + }, + "title": "Mostek Philips Hue" + } +} \ No newline at end of file diff --git a/homeassistant/components/hue/.translations/ro.json b/homeassistant/components/hue/.translations/ro.json new file mode 100644 index 00000000000..91541edcc7d --- /dev/null +++ b/homeassistant/components/hue/.translations/ro.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "linking": "A ap\u0103rut o eroare de leg\u0103tur\u0103 necunoscut\u0103.", + "register_failed": "Nu a reu\u0219it \u00eenregistrarea, \u00eencerca\u021bi din nou" + }, + "step": { + "init": { + "data": { + "host": "Gazd\u0103" + } + }, + "link": { + "description": "Ap\u0103sa\u021bi butonul de pe pod pentru a \u00eenregistra Philips Hue cu Home Assistant. \n\n ! [Loca\u021bia butonului pe pod] (/ static / images / config_philips_hue.jpg)" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hue/.translations/sl.json b/homeassistant/components/hue/.translations/sl.json new file mode 100644 index 00000000000..a6c858e0e40 --- /dev/null +++ b/homeassistant/components/hue/.translations/sl.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "all_configured": "Vsi mostovi Philips Hue so \u017ee konfigurirani", + "discover_timeout": "Ni bilo mogo\u010de odkriti Hue mostov", + "no_bridges": "Ni odkritih mostov Philips Hue" + }, + "error": { + "linking": "Pri\u0161lo je do neznane napake pri povezavi.", + "register_failed": "Registracija ni uspela, poskusite znova" + }, + "step": { + "init": { + "data": { + "host": "Host" + }, + "title": "Izberite Hue most" + }, + "link": { + "description": "Pritisnite gumb na mostu, da registrirate Philips Hue s Home Assistentom. \n\n ! [Polo\u017eaj gumba na mostu] (/static/images/config_philips_hue.jpg)", + "title": "Link Hub" + } + }, + "title": "Philips Hue Bridge" + } +} \ No newline at end of file diff --git a/homeassistant/components/hue/.translations/zh-Hans.json b/homeassistant/components/hue/.translations/zh-Hans.json new file mode 100644 index 00000000000..5a94e084dd2 --- /dev/null +++ b/homeassistant/components/hue/.translations/zh-Hans.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "all_configured": "\u5168\u90e8\u98de\u5229\u6d66 Hue \u6865\u63a5\u5668\u5df2\u914d\u7f6e", + "discover_timeout": "\u65e0\u6cd5\u55c5\u63a2 Hue \u6865\u63a5\u5668", + "no_bridges": "\u672a\u53d1\u73b0\u98de\u5229\u6d66 Hue Bridge" + }, + "error": { + "linking": "\u53d1\u751f\u672a\u77e5\u7684\u8fde\u63a5\u9519\u8bef\u3002", + "register_failed": "\u6ce8\u518c\u5931\u8d25\uff0c\u8bf7\u91cd\u8bd5" + }, + "step": { + "init": { + "data": { + "host": "\u4e3b\u673a" + }, + "title": "\u9009\u62e9 Hue Bridge" + }, + "link": { + "description": "\u8bf7\u6309\u4e0b\u6865\u63a5\u5668\u4e0a\u7684\u6309\u94ae\uff0c\u5728 Home Assistant \u4e0a\u6ce8\u518c\u98de\u5229\u6d66 Hue ![\u6865\u63a5\u5668\u6309\u94ae\u4f4d\u7f6e](/static/images/config_philips_hue.jpg)", + "title": "\u8fde\u63a5\u4e2d\u67a2" + } + }, + "title": "\u98de\u5229\u6d66 Hue Bridge" + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/season.cs.json b/homeassistant/components/sensor/.translations/season.cs.json new file mode 100644 index 00000000000..e2d7e7919be --- /dev/null +++ b/homeassistant/components/sensor/.translations/season.cs.json @@ -0,0 +1,8 @@ +{ + "state": { + "autumn": "Podzim", + "spring": "Jaro", + "summer": "L\u00e9to", + "winter": "Zima" + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/season.cy.json b/homeassistant/components/sensor/.translations/season.cy.json new file mode 100644 index 00000000000..0d1553ac3ea --- /dev/null +++ b/homeassistant/components/sensor/.translations/season.cy.json @@ -0,0 +1,8 @@ +{ + "state": { + "autumn": "Hydref", + "spring": "Gwanwyn", + "summer": "Haf", + "winter": "Gaeaf" + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/season.de.json b/homeassistant/components/sensor/.translations/season.de.json new file mode 100644 index 00000000000..50d702340b9 --- /dev/null +++ b/homeassistant/components/sensor/.translations/season.de.json @@ -0,0 +1,8 @@ +{ + "state": { + "autumn": "Herbst", + "spring": "Fr\u00fchling", + "summer": "Sommer", + "winter": "Winter" + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/season.es.json b/homeassistant/components/sensor/.translations/season.es.json new file mode 100644 index 00000000000..65df6a58b10 --- /dev/null +++ b/homeassistant/components/sensor/.translations/season.es.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.fi.json b/homeassistant/components/sensor/.translations/season.fi.json new file mode 100644 index 00000000000..f01f6451549 --- /dev/null +++ b/homeassistant/components/sensor/.translations/season.fi.json @@ -0,0 +1,8 @@ +{ + "state": { + "autumn": "Syksy", + "spring": "Kev\u00e4t", + "summer": "Kes\u00e4", + "winter": "Talvi" + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/season.ja.json b/homeassistant/components/sensor/.translations/season.ja.json new file mode 100644 index 00000000000..e441b1aa8ac --- /dev/null +++ b/homeassistant/components/sensor/.translations/season.ja.json @@ -0,0 +1,8 @@ +{ + "state": { + "autumn": "\u79cb", + "spring": "\u6625", + "summer": "\u590f", + "winter": "\u51ac" + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/season.ko.json b/homeassistant/components/sensor/.translations/season.ko.json new file mode 100644 index 00000000000..f2bf0a7bae5 --- /dev/null +++ b/homeassistant/components/sensor/.translations/season.ko.json @@ -0,0 +1,8 @@ +{ + "state": { + "autumn": "\uac00\uc744", + "spring": "\ubd04", + "summer": "\uc5ec\ub984", + "winter": "\uaca8\uc6b8" + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/season.nl.json b/homeassistant/components/sensor/.translations/season.nl.json new file mode 100644 index 00000000000..6054a8e2be5 --- /dev/null +++ b/homeassistant/components/sensor/.translations/season.nl.json @@ -0,0 +1,8 @@ +{ + "state": { + "autumn": "Herfst", + "spring": "Lente", + "summer": "Zomer", + "winter": "Winter" + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/season.no.json b/homeassistant/components/sensor/.translations/season.no.json new file mode 100644 index 00000000000..9d520dae6a5 --- /dev/null +++ b/homeassistant/components/sensor/.translations/season.no.json @@ -0,0 +1,8 @@ +{ + "state": { + "autumn": "H\u00f8st", + "spring": "V\u00e5r", + "summer": "Sommer", + "winter": "Vinter" + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/season.pl.json b/homeassistant/components/sensor/.translations/season.pl.json new file mode 100644 index 00000000000..f5a7da57e7f --- /dev/null +++ b/homeassistant/components/sensor/.translations/season.pl.json @@ -0,0 +1,8 @@ +{ + "state": { + "autumn": "Jesie\u0144", + "spring": "Wiosna", + "summer": "Lato", + "winter": "Zima" + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/season.pt.json b/homeassistant/components/sensor/.translations/season.pt.json new file mode 100644 index 00000000000..fde45ad6c8e --- /dev/null +++ b/homeassistant/components/sensor/.translations/season.pt.json @@ -0,0 +1,8 @@ +{ + "state": { + "autumn": "Outono", + "spring": "Primavera", + "summer": "Ver\u00e3o", + "winter": "Inverno" + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/season.ro.json b/homeassistant/components/sensor/.translations/season.ro.json new file mode 100644 index 00000000000..04f90318290 --- /dev/null +++ b/homeassistant/components/sensor/.translations/season.ro.json @@ -0,0 +1,8 @@ +{ + "state": { + "autumn": "Toamn\u0103", + "spring": "Prim\u0103var\u0103", + "summer": "Var\u0103", + "winter": "Iarn\u0103" + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/season.sl.json b/homeassistant/components/sensor/.translations/season.sl.json new file mode 100644 index 00000000000..f715a3ec13a --- /dev/null +++ b/homeassistant/components/sensor/.translations/season.sl.json @@ -0,0 +1,8 @@ +{ + "state": { + "autumn": "Jesen", + "spring": "Pomlad", + "summer": "Poletje", + "winter": "Zima" + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/season.sv.json b/homeassistant/components/sensor/.translations/season.sv.json new file mode 100644 index 00000000000..02332d76906 --- /dev/null +++ b/homeassistant/components/sensor/.translations/season.sv.json @@ -0,0 +1,8 @@ +{ + "state": { + "autumn": "H\u00f6st", + "spring": "V\u00e5r", + "summer": "Sommar", + "winter": "Vinter" + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/season.th.json b/homeassistant/components/sensor/.translations/season.th.json new file mode 100644 index 00000000000..09799730389 --- /dev/null +++ b/homeassistant/components/sensor/.translations/season.th.json @@ -0,0 +1,8 @@ +{ + "state": { + "autumn": "\u0e24\u0e14\u0e39\u0e43\u0e1a\u0e44\u0e21\u0e49\u0e23\u0e48\u0e27\u0e07", + "spring": "\u0e24\u0e14\u0e39\u0e43\u0e1a\u0e44\u0e21\u0e49\u0e1c\u0e25\u0e34", + "summer": "\u0e24\u0e14\u0e39\u0e23\u0e49\u0e2d\u0e19", + "winter": "\u0e24\u0e14\u0e39\u0e2b\u0e19\u0e32\u0e27" + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/season.zh-Hans.json b/homeassistant/components/sensor/.translations/season.zh-Hans.json new file mode 100644 index 00000000000..78801f4b1df --- /dev/null +++ b/homeassistant/components/sensor/.translations/season.zh-Hans.json @@ -0,0 +1,8 @@ +{ + "state": { + "autumn": "\u79cb\u5b63", + "spring": "\u6625\u5b63", + "summer": "\u590f\u5b63", + "winter": "\u51ac\u5b63" + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/season.zh-Hant.json b/homeassistant/components/sensor/.translations/season.zh-Hant.json new file mode 100644 index 00000000000..78801f4b1df --- /dev/null +++ b/homeassistant/components/sensor/.translations/season.zh-Hant.json @@ -0,0 +1,8 @@ +{ + "state": { + "autumn": "\u79cb\u5b63", + "spring": "\u6625\u5b63", + "summer": "\u590f\u5b63", + "winter": "\u51ac\u5b63" + } +} \ No newline at end of file From e122692b46715820dc4e56460d367a9fbc99b777 Mon Sep 17 00:00:00 2001 From: Kane610 Date: Thu, 15 Mar 2018 04:07:37 +0100 Subject: [PATCH 090/220] deCONZ - Add support for consumption and power sensors (#13218) * Add support for consumption and power sensors * Keep attr_current inside component --- homeassistant/components/binary_sensor/deconz.py | 11 +++++------ homeassistant/components/deconz/__init__.py | 2 +- homeassistant/components/sensor/deconz.py | 16 ++++++++++------ requirements_all.txt | 2 +- 4 files changed, 17 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/binary_sensor/deconz.py b/homeassistant/components/binary_sensor/deconz.py index 1effcf1800a..ef3ec506e3a 100644 --- a/homeassistant/components/binary_sensor/deconz.py +++ b/homeassistant/components/binary_sensor/deconz.py @@ -23,8 +23,7 @@ async def async_setup_platform(hass, config, async_add_devices, sensors = hass.data[DATA_DECONZ].sensors entities = [] - for key in sorted(sensors.keys(), key=int): - sensor = sensors[key] + for sensor in sensors.values(): if sensor and sensor.type in DECONZ_BINARY_SENSOR: entities.append(DeconzBinarySensor(sensor)) async_add_devices(entities, True) @@ -93,9 +92,9 @@ class DeconzBinarySensor(BinarySensorDevice): def device_state_attributes(self): """Return the state attributes of the sensor.""" from pydeconz.sensor import PRESENCE - attr = { - ATTR_BATTERY_LEVEL: self._sensor.battery, - } - if self._sensor.type in PRESENCE: + attr = {} + if self._sensor.battery: + attr[ATTR_BATTERY_LEVEL] = self._sensor.battery + if self._sensor.type in PRESENCE and self._sensor.dark: attr['dark'] = self._sensor.dark return attr diff --git a/homeassistant/components/deconz/__init__.py b/homeassistant/components/deconz/__init__.py index de6d3e89859..26d9fb401e4 100644 --- a/homeassistant/components/deconz/__init__.py +++ b/homeassistant/components/deconz/__init__.py @@ -17,7 +17,7 @@ from homeassistant.helpers import discovery from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.util.json import load_json, save_json -REQUIREMENTS = ['pydeconz==31'] +REQUIREMENTS = ['pydeconz==32'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sensor/deconz.py b/homeassistant/components/sensor/deconz.py index a3c2aa683dc..081b304dc55 100644 --- a/homeassistant/components/sensor/deconz.py +++ b/homeassistant/components/sensor/deconz.py @@ -6,7 +6,8 @@ https://home-assistant.io/components/sensor.deconz/ """ from homeassistant.components.deconz import ( DOMAIN as DATA_DECONZ, DATA_DECONZ_ID) -from homeassistant.const import ATTR_BATTERY_LEVEL, CONF_EVENT, CONF_ID +from homeassistant.const import ( + ATTR_BATTERY_LEVEL, ATTR_VOLTAGE, CONF_EVENT, CONF_ID) from homeassistant.core import EventOrigin, callback from homeassistant.helpers.entity import Entity from homeassistant.helpers.icon import icon_for_battery_level @@ -14,6 +15,7 @@ from homeassistant.util import slugify DEPENDENCIES = ['deconz'] +ATTR_CURRENT = 'current' ATTR_EVENT_ID = 'event_id' @@ -27,8 +29,7 @@ async def async_setup_platform(hass, config, async_add_devices, sensors = hass.data[DATA_DECONZ].sensors entities = [] - for key in sorted(sensors.keys(), key=int): - sensor = sensors[key] + for sensor in sensors.values(): if sensor and sensor.type in DECONZ_SENSOR: if sensor.type in DECONZ_REMOTE: DeconzEvent(hass, sensor) @@ -106,9 +107,12 @@ class DeconzSensor(Entity): @property def device_state_attributes(self): """Return the state attributes of the sensor.""" - attr = { - ATTR_BATTERY_LEVEL: self._sensor.battery, - } + attr = {} + if self._sensor.battery: + attr[ATTR_BATTERY_LEVEL] = self._sensor.battery + if self.unit_of_measurement == 'Watts': + attr[ATTR_CURRENT] = self._sensor.current + attr[ATTR_VOLTAGE] = self._sensor.voltage return attr diff --git a/requirements_all.txt b/requirements_all.txt index b35b3d0991a..608618eb166 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -701,7 +701,7 @@ pycsspeechtts==1.0.2 pydaikin==0.4 # homeassistant.components.deconz -pydeconz==31 +pydeconz==32 # homeassistant.components.zwave pydispatcher==2.0.5 From c971d61422d2dffb0102c63b3e82870b5eac4cdd Mon Sep 17 00:00:00 2001 From: c727 Date: Thu, 15 Mar 2018 04:56:56 +0100 Subject: [PATCH 091/220] Change Hass.io icon to home-assistant (#13230) --- 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 540659273b3..87251a2745c 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -156,7 +156,7 @@ def async_setup(hass, config): if 'frontend' in hass.config.components: yield from hass.components.frontend.async_register_built_in_panel( - 'hassio', 'Hass.io', 'mdi:access-point-network') + 'hassio', 'Hass.io', 'mdi:home-assistant') if 'http' in config: yield from hassio.update_hass_api(config['http']) From 223bc187dcf047445b44154ab215e1edd5576e02 Mon Sep 17 00:00:00 2001 From: PhracturedBlue Date: Wed, 14 Mar 2018 21:44:13 -0700 Subject: [PATCH 092/220] More robust MJPEG parser. Fixes #13138. (#13226) * More robust MJPEG parser. Fixes ##13138. * Reimplement image extraction from mjpeg without ascy generator to support python 3.5 --- homeassistant/components/camera/proxy.py | 52 ++++++++---------------- 1 file changed, 17 insertions(+), 35 deletions(-) diff --git a/homeassistant/components/camera/proxy.py b/homeassistant/components/camera/proxy.py index d045235c3ad..1984c21fadb 100644 --- a/homeassistant/components/camera/proxy.py +++ b/homeassistant/components/camera/proxy.py @@ -56,34 +56,6 @@ async def async_setup_platform(hass, config, async_add_devices, async_add_devices([ProxyCamera(hass, config)]) -async def _read_frame(req): - """Read a single frame from an MJPEG stream.""" - # based on https://gist.github.com/russss/1143799 - import cgi - # Read in HTTP headers: - stream = req.content - # multipart/x-mixed-replace; boundary=--frameboundary - _mimetype, options = cgi.parse_header(req.headers['content-type']) - boundary = options.get('boundary').encode('utf-8') - if not boundary: - _LOGGER.error("Malformed MJPEG missing boundary") - raise Exception("Can't find content-type") - - line = await stream.readline() - # Seek ahead to the first chunk - while line.strip() != boundary: - line = await stream.readline() - # Read in chunk headers - while line.strip() != b'': - parts = line.split(b':') - if len(parts) > 1 and parts[0].lower() == b'content-length': - # Grab chunk length - length = int(parts[1].strip()) - line = await stream.readline() - image = await stream.read(length) - return image - - def _resize_image(image, opts): """Resize image.""" from PIL import Image @@ -227,9 +199,9 @@ class ProxyCamera(Camera): 'boundary=--frameboundary') await response.prepare(request) - def write(img_bytes): + async def write(img_bytes): """Write image to stream.""" - response.write(bytes( + await response.write(bytes( '--frameboundary\r\n' 'Content-Type: {}\r\n' 'Content-Length: {}\r\n\r\n'.format( @@ -240,13 +212,23 @@ class ProxyCamera(Camera): req = await stream_coro try: + # This would be nicer as an async generator + # But that would only be supported for python >=3.6 + data = b'' + stream = req.content while True: - image = await _read_frame(req) - if not image: + chunk = await stream.read(102400) + if not chunk: break - image = await self.hass.async_add_job( - _resize_image, image, self._stream_opts) - write(image) + data += chunk + jpg_start = data.find(b'\xff\xd8') + jpg_end = data.find(b'\xff\xd9') + if jpg_start != -1 and jpg_end != -1: + image = data[jpg_start:jpg_end + 2] + image = await self.hass.async_add_job( + _resize_image, image, self._stream_opts) + await write(image) + data = data[jpg_end + 2:] except asyncio.CancelledError: _LOGGER.debug("Stream closed by frontend.") req.close() From 6909be1cc74a44f8c6deaa79ab491ee2ceb7d030 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Thu, 15 Mar 2018 11:45:54 +0100 Subject: [PATCH 093/220] Add docstring (#13232) --- homeassistant/components/sensor/crimereports.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/sensor/crimereports.py b/homeassistant/components/sensor/crimereports.py index aecfca60bf1..a2d7315a314 100644 --- a/homeassistant/components/sensor/crimereports.py +++ b/homeassistant/components/sensor/crimereports.py @@ -89,6 +89,7 @@ class CrimeReportsSensor(Entity): return self._attributes def _incident_event(self, incident): + """Fire if an event occurs.""" data = { 'type': incident.get('type'), 'description': incident.get('friendly_description'), From 27c18068971096219b720e0f18ebb68cde34739e Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Thu, 15 Mar 2018 12:10:54 +0100 Subject: [PATCH 094/220] Python 3.5 adjustments (#13173) --- homeassistant/bootstrap.py | 8 -------- homeassistant/components/mysensors.py | 5 ++--- homeassistant/core.py | 6 +----- homeassistant/monkey_patch.py | 2 +- homeassistant/scripts/check_config.py | 9 ++++++--- tests/scripts/test_check_config.py | 4 +--- 6 files changed, 11 insertions(+), 23 deletions(-) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 34eab679581..00822d93299 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -86,14 +86,6 @@ def async_from_config_dict(config: Dict[str, Any], if enable_log: async_enable_logging(hass, verbose, log_rotate_days, log_file) - if sys.version_info[:2] < (3, 5): - _LOGGER.warning( - 'Python 3.4 support has been deprecated and will be removed in ' - 'the beginning of 2018. Please upgrade Python or your operating ' - 'system. More info: https://home-assistant.io/blog/2017/10/06/' - 'deprecating-python-3.4-support/' - ) - core_config = config.get(core.DOMAIN, {}) try: diff --git a/homeassistant/components/mysensors.py b/homeassistant/components/mysensors.py index 37e257e5eb9..a560b49648f 100644 --- a/homeassistant/components/mysensors.py +++ b/homeassistant/components/mysensors.py @@ -545,9 +545,8 @@ def setup_mysensors_platform( device_class_copy = device_class[s_type] name = get_mysensors_name(gateway, node_id, child_id) - # python 3.4 cannot unpack inside tuple, but combining tuples works - args_copy = device_args + ( - gateway, node_id, child_id, name, value_type) + args_copy = (*device_args, gateway, node_id, child_id, name, + value_type) devices[dev_id] = device_class_copy(*args_copy) new_devices.append(devices[dev_id]) if new_devices: diff --git a/homeassistant/core.py b/homeassistant/core.py index a486ee1adbf..b49b94f853d 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -117,11 +117,7 @@ class HomeAssistant(object): else: self.loop = loop or asyncio.get_event_loop() - executor_opts = {'max_workers': 10} - if sys.version_info[:2] >= (3, 5): - # It will default set to the number of processors on the machine, - # multiplied by 5. That is better for overlap I/O workers. - executor_opts['max_workers'] = None + executor_opts = {'max_workers': None} if sys.version_info[:2] >= (3, 6): executor_opts['thread_name_prefix'] = 'SyncWorker' diff --git a/homeassistant/monkey_patch.py b/homeassistant/monkey_patch.py index 5aa051f2bb5..d5c629c9d34 100644 --- a/homeassistant/monkey_patch.py +++ b/homeassistant/monkey_patch.py @@ -61,7 +61,7 @@ def disable_c_asyncio(): def find_module(self, fullname, path=None): """Find a module.""" if fullname == self.PATH_TRIGGER: - # We lint in Py34, exception is introduced in Py36 + # We lint in Py35, exception is introduced in Py36 # pylint: disable=undefined-variable raise ModuleNotFoundError() # noqa return None diff --git a/homeassistant/scripts/check_config.py b/homeassistant/scripts/check_config.py index 1a58757d17f..ac3ac62e82d 100644 --- a/homeassistant/scripts/check_config.py +++ b/homeassistant/scripts/check_config.py @@ -95,9 +95,12 @@ def run(script_args: List) -> int: if args.files: print(color(C_HEAD, 'yaml files'), '(used /', color('red', 'not used') + ')') - # Python 3.5 gets a recursive, but not in 3.4 - for yfn in sorted(glob(os.path.join(config_dir, '*.yaml')) + - glob(os.path.join(config_dir, '*/*.yaml'))): + deps = os.path.join(config_dir, 'deps') + yaml_files = [f for f in glob(os.path.join(config_dir, '**/*.yaml'), + recursive=True) + if not f.startswith(deps)] + + for yfn in sorted(yaml_files): the_color = '' if yfn in res['yaml_files'] else 'red' print(color(the_color, '-', yfn)) diff --git a/tests/scripts/test_check_config.py b/tests/scripts/test_check_config.py index 677ed8de110..28a3f2ebdc8 100644 --- a/tests/scripts/test_check_config.py +++ b/tests/scripts/test_check_config.py @@ -41,9 +41,7 @@ class TestCheckConfig(unittest.TestCase): # this ensures we have one. try: asyncio.get_event_loop() - except (RuntimeError, AssertionError): - # Py35: RuntimeError - # Py34: AssertionError + except RuntimeError: asyncio.set_event_loop(asyncio.new_event_loop()) # Will allow seeing full diff From 646ed5de528a2227ac1239426305f1a3abb0d3b9 Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Thu, 15 Mar 2018 12:31:31 +0100 Subject: [PATCH 095/220] Added cover.group platform (replaces #12303) (#12692) * Added cover.group platform * Added async/await, smaller changes * Made (async_update) methods regular methods * Small improvements * Changed classname * Changes based on feedback * Service calls * update_supported_features is now a callback method * combined all 'update_attr_*' methods in 'async_update' * Small changes * Fixes * is_closed * current_position * current_tilt_position * Updated tests * Small changes 2 --- CODEOWNERS | 1 + homeassistant/components/cover/group.py | 271 ++++++++++++++++++ tests/components/cover/test_group.py | 350 ++++++++++++++++++++++++ 3 files changed, 622 insertions(+) create mode 100755 homeassistant/components/cover/group.py create mode 100644 tests/components/cover/test_group.py diff --git a/CODEOWNERS b/CODEOWNERS index fedab8f6ae4..d8ebc3cff56 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -49,6 +49,7 @@ homeassistant/components/camera/yi.py @bachya homeassistant/components/climate/ephember.py @ttroy50 homeassistant/components/climate/eq3btsmart.py @rytilahti homeassistant/components/climate/sensibo.py @andrey-git +homeassistant/components/cover/group.py @cdce8p homeassistant/components/cover/template.py @PhracturedBlue homeassistant/components/device_tracker/automatic.py @armills homeassistant/components/device_tracker/tile.py @bachya diff --git a/homeassistant/components/cover/group.py b/homeassistant/components/cover/group.py new file mode 100755 index 00000000000..c1ea33a9cc7 --- /dev/null +++ b/homeassistant/components/cover/group.py @@ -0,0 +1,271 @@ +""" +This platform allows several cover to be grouped into one cover. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/cover.group/ +""" +import logging + +import voluptuous as vol + +from homeassistant.core import callback +from homeassistant.components.cover import ( + DOMAIN, PLATFORM_SCHEMA, CoverDevice, ATTR_POSITION, + ATTR_CURRENT_POSITION, ATTR_TILT_POSITION, ATTR_CURRENT_TILT_POSITION, + SUPPORT_OPEN, SUPPORT_CLOSE, SUPPORT_STOP, SUPPORT_SET_POSITION, + SUPPORT_OPEN_TILT, SUPPORT_CLOSE_TILT, + SUPPORT_STOP_TILT, SUPPORT_SET_TILT_POSITION, + SERVICE_OPEN_COVER, SERVICE_CLOSE_COVER, SERVICE_SET_COVER_POSITION, + SERVICE_STOP_COVER, SERVICE_OPEN_COVER_TILT, SERVICE_CLOSE_COVER_TILT, + SERVICE_STOP_COVER_TILT, SERVICE_SET_COVER_TILT_POSITION) +from homeassistant.const import ( + ATTR_ASSUMED_STATE, ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, + CONF_ENTITIES, CONF_NAME, STATE_CLOSED) +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.event import async_track_state_change + +_LOGGER = logging.getLogger(__name__) + +KEY_OPEN_CLOSE = 'open_close' +KEY_STOP = 'stop' +KEY_POSITION = 'position' + +DEFAULT_NAME = 'Cover Group' + + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Required(CONF_ENTITIES): cv.entities_domain(DOMAIN), +}) + + +async def async_setup_platform(hass, config, async_add_devices, + discovery_info=None): + """Set up the Group Cover platform.""" + async_add_devices( + [CoverGroup(config[CONF_NAME], config[CONF_ENTITIES])]) + + +class CoverGroup(CoverDevice): + """Representation of a CoverGroup.""" + + def __init__(self, name, entities): + """Initialize a CoverGroup entity.""" + self._name = name + self._is_closed = False + self._cover_position = 100 + self._tilt_position = None + self._supported_features = 0 + self._assumed_state = True + + self._entities = entities + self._covers = {KEY_OPEN_CLOSE: set(), KEY_STOP: set(), + KEY_POSITION: set()} + self._tilts = {KEY_OPEN_CLOSE: set(), KEY_STOP: set(), + KEY_POSITION: set()} + + @callback + def update_supported_features(self, entity_id, old_state, new_state, + update_state=True): + """Update dictionaries with supported features.""" + if not new_state: + for values in self._covers.values(): + values.discard(entity_id) + for values in self._tilts.values(): + values.discard(entity_id) + if update_state: + self.async_schedule_update_ha_state(True) + return + + features = new_state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) + + if features & (SUPPORT_OPEN | SUPPORT_CLOSE): + self._covers[KEY_OPEN_CLOSE].add(entity_id) + else: + self._covers[KEY_OPEN_CLOSE].discard(entity_id) + if features & (SUPPORT_STOP): + self._covers[KEY_STOP].add(entity_id) + else: + self._covers[KEY_STOP].discard(entity_id) + if features & (SUPPORT_SET_POSITION): + self._covers[KEY_POSITION].add(entity_id) + else: + self._covers[KEY_POSITION].discard(entity_id) + + if features & (SUPPORT_OPEN_TILT | SUPPORT_CLOSE_TILT): + self._tilts[KEY_OPEN_CLOSE].add(entity_id) + else: + self._tilts[KEY_OPEN_CLOSE].discard(entity_id) + if features & (SUPPORT_STOP_TILT): + self._tilts[KEY_STOP].add(entity_id) + else: + self._tilts[KEY_STOP].discard(entity_id) + if features & (SUPPORT_SET_TILT_POSITION): + self._tilts[KEY_POSITION].add(entity_id) + else: + self._tilts[KEY_POSITION].discard(entity_id) + + if update_state: + self.async_schedule_update_ha_state(True) + + async def async_added_to_hass(self): + """Register listeners.""" + for entity_id in self._entities: + new_state = self.hass.states.get(entity_id) + self.update_supported_features(entity_id, None, new_state, + update_state=False) + async_track_state_change(self.hass, self._entities, + self.update_supported_features) + await self.async_update() + + @property + def name(self): + """Return the name of the cover.""" + return self._name + + @property + def assumed_state(self): + """Enable buttons even if at end position.""" + return self._assumed_state + + @property + def should_poll(self): + """Disable polling for cover group.""" + return False + + @property + def supported_features(self): + """Flag supported features for the cover.""" + return self._supported_features + + @property + def is_closed(self): + """Return if all covers in group are closed.""" + return self._is_closed + + @property + def current_cover_position(self): + """Return current position for all covers.""" + return self._cover_position + + @property + def current_cover_tilt_position(self): + """Return current tilt position for all covers.""" + return self._tilt_position + + async def async_open_cover(self, **kwargs): + """Move the covers up.""" + data = {ATTR_ENTITY_ID: self._covers[KEY_OPEN_CLOSE]} + await self.hass.services.async_call( + DOMAIN, SERVICE_OPEN_COVER, data, blocking=True) + + async def async_close_cover(self, **kwargs): + """Move the covers down.""" + data = {ATTR_ENTITY_ID: self._covers[KEY_OPEN_CLOSE]} + await self.hass.services.async_call( + DOMAIN, SERVICE_CLOSE_COVER, data, blocking=True) + + async def async_stop_cover(self, **kwargs): + """Fire the stop action.""" + data = {ATTR_ENTITY_ID: self._covers[KEY_STOP]} + await self.hass.services.async_call( + DOMAIN, SERVICE_STOP_COVER, data, blocking=True) + + async def async_set_cover_position(self, **kwargs): + """Set covers position.""" + data = {ATTR_ENTITY_ID: self._covers[KEY_POSITION], + ATTR_POSITION: kwargs[ATTR_POSITION]} + await self.hass.services.async_call( + DOMAIN, SERVICE_SET_COVER_POSITION, data, blocking=True) + + async def async_open_cover_tilt(self, **kwargs): + """Tilt covers open.""" + data = {ATTR_ENTITY_ID: self._tilts[KEY_OPEN_CLOSE]} + await self.hass.services.async_call( + DOMAIN, SERVICE_OPEN_COVER_TILT, data, blocking=True) + + async def async_close_cover_tilt(self, **kwargs): + """Tilt covers closed.""" + data = {ATTR_ENTITY_ID: self._tilts[KEY_OPEN_CLOSE]} + await self.hass.services.async_call( + DOMAIN, SERVICE_CLOSE_COVER_TILT, data, blocking=True) + + async def async_stop_cover_tilt(self, **kwargs): + """Stop cover tilt.""" + data = {ATTR_ENTITY_ID: self._tilts[KEY_STOP]} + await self.hass.services.async_call( + DOMAIN, SERVICE_STOP_COVER_TILT, data, blocking=True) + + async def async_set_cover_tilt_position(self, **kwargs): + """Set tilt position.""" + data = {ATTR_ENTITY_ID: self._tilts[KEY_POSITION], + ATTR_TILT_POSITION: kwargs[ATTR_TILT_POSITION]} + await self.hass.services.async_call( + DOMAIN, SERVICE_SET_COVER_TILT_POSITION, data, blocking=True) + + async def async_update(self): + """Update state and attributes.""" + self._assumed_state = False + + self._is_closed = True + for entity_id in self._entities: + state = self.hass.states.get(entity_id) + if not state: + continue + if state.state != STATE_CLOSED: + self._is_closed = False + break + + self._cover_position = None + if self._covers[KEY_POSITION]: + position = -1 + self._cover_position = 0 if self.is_closed else 100 + for entity_id in self._covers[KEY_POSITION]: + state = self.hass.states.get(entity_id) + pos = state.attributes.get(ATTR_CURRENT_POSITION) + if position == -1: + position = pos + elif position != pos: + self._assumed_state = True + break + else: + if position != -1: + self._cover_position = position + + self._tilt_position = None + if self._tilts[KEY_POSITION]: + position = -1 + self._tilt_position = 100 + for entity_id in self._tilts[KEY_POSITION]: + state = self.hass.states.get(entity_id) + pos = state.attributes.get(ATTR_CURRENT_TILT_POSITION) + if position == -1: + position = pos + elif position != pos: + self._assumed_state = True + break + else: + if position != -1: + self._tilt_position = position + + supported_features = 0 + supported_features |= SUPPORT_OPEN | SUPPORT_CLOSE \ + if self._covers[KEY_OPEN_CLOSE] else 0 + supported_features |= SUPPORT_STOP \ + if self._covers[KEY_STOP] else 0 + supported_features |= SUPPORT_SET_POSITION \ + if self._covers[KEY_POSITION] else 0 + supported_features |= SUPPORT_OPEN_TILT | SUPPORT_CLOSE_TILT \ + if self._tilts[KEY_OPEN_CLOSE] else 0 + supported_features |= SUPPORT_STOP_TILT \ + if self._tilts[KEY_STOP] else 0 + supported_features |= SUPPORT_SET_TILT_POSITION \ + if self._tilts[KEY_POSITION] else 0 + self._supported_features = supported_features + + if not self._assumed_state: + for entity_id in self._entities: + state = self.hass.states.get(entity_id) + if state and state.attributes.get(ATTR_ASSUMED_STATE): + self._assumed_state = True + break diff --git a/tests/components/cover/test_group.py b/tests/components/cover/test_group.py new file mode 100644 index 00000000000..288e1c5e047 --- /dev/null +++ b/tests/components/cover/test_group.py @@ -0,0 +1,350 @@ +"""The tests for the group cover platform.""" + +import unittest +from datetime import timedelta +import homeassistant.util.dt as dt_util + +from homeassistant import setup +from homeassistant.components import cover +from homeassistant.components.cover import ( + ATTR_CURRENT_POSITION, ATTR_CURRENT_TILT_POSITION, DOMAIN) +from homeassistant.components.cover.group import DEFAULT_NAME +from homeassistant.const import ( + ATTR_ASSUMED_STATE, ATTR_FRIENDLY_NAME, ATTR_SUPPORTED_FEATURES, + CONF_ENTITIES, STATE_OPEN, STATE_CLOSED) +from tests.common import ( + assert_setup_component, get_test_home_assistant, fire_time_changed) + +COVER_GROUP = 'cover.cover_group' +DEMO_COVER = 'cover.kitchen_window' +DEMO_COVER_POS = 'cover.hall_window' +DEMO_COVER_TILT = 'cover.living_room_window' +DEMO_TILT = 'cover.tilt_demo' + +CONFIG = { + DOMAIN: [ + {'platform': 'demo'}, + {'platform': 'group', + CONF_ENTITIES: [ + DEMO_COVER, DEMO_COVER_POS, DEMO_COVER_TILT, DEMO_TILT]} + ] +} + + +class TestMultiCover(unittest.TestCase): + """Test the group cover platform.""" + + def setUp(self): + """Setup things to be run when tests are started.""" + self.hass = get_test_home_assistant() + + def tearDown(self): + """Stop down everything that was started.""" + self.hass.stop() + + def test_attributes(self): + """Test handling of state attributes.""" + config = {DOMAIN: {'platform': 'group', CONF_ENTITIES: [ + DEMO_COVER, DEMO_COVER_POS, DEMO_COVER_TILT, DEMO_TILT]}} + + with assert_setup_component(1, DOMAIN): + assert setup.setup_component(self.hass, DOMAIN, config) + + state = self.hass.states.get(COVER_GROUP) + attr = state.attributes + self.assertEqual(state.state, STATE_CLOSED) + self.assertEqual(attr.get(ATTR_FRIENDLY_NAME), DEFAULT_NAME) + self.assertEqual(attr.get(ATTR_ASSUMED_STATE), None) + self.assertEqual(attr.get(ATTR_SUPPORTED_FEATURES), 0) + self.assertEqual(attr.get(ATTR_CURRENT_POSITION), None) + self.assertEqual(attr.get(ATTR_CURRENT_TILT_POSITION), None) + + # Add Entity that supports open / close / stop + self.hass.states.set( + DEMO_COVER, STATE_OPEN, {ATTR_SUPPORTED_FEATURES: 11}) + self.hass.block_till_done() + + state = self.hass.states.get(COVER_GROUP) + attr = state.attributes + self.assertEqual(state.state, STATE_OPEN) + self.assertEqual(attr.get(ATTR_ASSUMED_STATE), None) + self.assertEqual(attr.get(ATTR_SUPPORTED_FEATURES), 11) + self.assertEqual(attr.get(ATTR_CURRENT_POSITION), None) + self.assertEqual(attr.get(ATTR_CURRENT_TILT_POSITION), None) + + # Add Entity that supports set_cover_position + self.hass.states.set( + DEMO_COVER_POS, STATE_OPEN, + {ATTR_SUPPORTED_FEATURES: 4, ATTR_CURRENT_POSITION: 70}) + self.hass.block_till_done() + + state = self.hass.states.get(COVER_GROUP) + attr = state.attributes + self.assertEqual(state.state, STATE_OPEN) + self.assertEqual(attr.get(ATTR_ASSUMED_STATE), None) + self.assertEqual(attr.get(ATTR_SUPPORTED_FEATURES), 15) + self.assertEqual(attr.get(ATTR_CURRENT_POSITION), 70) + self.assertEqual(attr.get(ATTR_CURRENT_TILT_POSITION), None) + + # Add Entity that supports open tilt / close tilt / stop tilt + self.hass.states.set( + DEMO_TILT, STATE_OPEN, {ATTR_SUPPORTED_FEATURES: 112}) + self.hass.block_till_done() + + state = self.hass.states.get(COVER_GROUP) + attr = state.attributes + self.assertEqual(state.state, STATE_OPEN) + self.assertEqual(attr.get(ATTR_ASSUMED_STATE), None) + self.assertEqual(attr.get(ATTR_SUPPORTED_FEATURES), 127) + self.assertEqual(attr.get(ATTR_CURRENT_POSITION), 70) + self.assertEqual(attr.get(ATTR_CURRENT_TILT_POSITION), None) + + # Add Entity that supports set_tilt_position + self.hass.states.set( + DEMO_COVER_TILT, STATE_OPEN, + {ATTR_SUPPORTED_FEATURES: 128, ATTR_CURRENT_TILT_POSITION: 60}) + self.hass.block_till_done() + + state = self.hass.states.get(COVER_GROUP) + attr = state.attributes + self.assertEqual(state.state, STATE_OPEN) + self.assertEqual(attr.get(ATTR_ASSUMED_STATE), None) + self.assertEqual(attr.get(ATTR_SUPPORTED_FEATURES), 255) + self.assertEqual(attr.get(ATTR_CURRENT_POSITION), 70) + self.assertEqual(attr.get(ATTR_CURRENT_TILT_POSITION), 60) + + # ### Test assumed state ### + # ########################## + + # For covers + self.hass.states.set( + DEMO_COVER, STATE_OPEN, + {ATTR_SUPPORTED_FEATURES: 4, ATTR_CURRENT_POSITION: 100}) + self.hass.block_till_done() + + state = self.hass.states.get(COVER_GROUP) + attr = state.attributes + self.assertEqual(state.state, STATE_OPEN) + self.assertEqual(attr.get(ATTR_ASSUMED_STATE), True) + self.assertEqual(attr.get(ATTR_SUPPORTED_FEATURES), 244) + self.assertEqual(attr.get(ATTR_CURRENT_POSITION), 100) + self.assertEqual(attr.get(ATTR_CURRENT_TILT_POSITION), 60) + + self.hass.states.remove(DEMO_COVER) + self.hass.block_till_done() + self.hass.states.remove(DEMO_COVER_POS) + self.hass.block_till_done() + + state = self.hass.states.get(COVER_GROUP) + attr = state.attributes + self.assertEqual(state.state, STATE_OPEN) + self.assertEqual(attr.get(ATTR_ASSUMED_STATE), None) + self.assertEqual(attr.get(ATTR_SUPPORTED_FEATURES), 240) + self.assertEqual(attr.get(ATTR_CURRENT_POSITION), None) + self.assertEqual(attr.get(ATTR_CURRENT_TILT_POSITION), 60) + + # For tilts + self.hass.states.set( + DEMO_TILT, STATE_OPEN, + {ATTR_SUPPORTED_FEATURES: 128, ATTR_CURRENT_TILT_POSITION: 100}) + self.hass.block_till_done() + + state = self.hass.states.get(COVER_GROUP) + attr = state.attributes + self.assertEqual(state.state, STATE_OPEN) + self.assertEqual(attr.get(ATTR_ASSUMED_STATE), True) + self.assertEqual(attr.get(ATTR_SUPPORTED_FEATURES), 128) + self.assertEqual(attr.get(ATTR_CURRENT_POSITION), None) + self.assertEqual(attr.get(ATTR_CURRENT_TILT_POSITION), 100) + + self.hass.states.remove(DEMO_COVER_TILT) + self.hass.states.set(DEMO_TILT, STATE_CLOSED) + self.hass.block_till_done() + + state = self.hass.states.get(COVER_GROUP) + attr = state.attributes + self.assertEqual(state.state, STATE_CLOSED) + self.assertEqual(attr.get(ATTR_ASSUMED_STATE), None) + self.assertEqual(attr.get(ATTR_SUPPORTED_FEATURES), 0) + self.assertEqual(attr.get(ATTR_CURRENT_POSITION), None) + self.assertEqual(attr.get(ATTR_CURRENT_TILT_POSITION), None) + + self.hass.states.set( + DEMO_TILT, STATE_CLOSED, {ATTR_ASSUMED_STATE: True}) + self.hass.block_till_done() + + state = self.hass.states.get(COVER_GROUP) + attr = state.attributes + self.assertEqual(attr.get(ATTR_ASSUMED_STATE), True) + + def test_open_covers(self): + """Test open cover function.""" + with assert_setup_component(2, DOMAIN): + assert setup.setup_component(self.hass, DOMAIN, CONFIG) + + cover.open_cover(self.hass, COVER_GROUP) + self.hass.block_till_done() + for _ in range(10): + future = dt_util.utcnow() + timedelta(seconds=1) + fire_time_changed(self.hass, future) + self.hass.block_till_done() + + state = self.hass.states.get(COVER_GROUP) + self.assertEqual(state.state, STATE_OPEN) + self.assertEqual(state.attributes.get(ATTR_CURRENT_POSITION), 100) + + self.assertEqual(self.hass.states.get(DEMO_COVER).state, STATE_OPEN) + self.assertEqual(self.hass.states.get(DEMO_COVER_POS) + .attributes.get(ATTR_CURRENT_POSITION), 100) + self.assertEqual(self.hass.states.get(DEMO_COVER_TILT) + .attributes.get(ATTR_CURRENT_POSITION), 100) + + def test_close_covers(self): + """Test close cover function.""" + with assert_setup_component(2, DOMAIN): + assert setup.setup_component(self.hass, DOMAIN, CONFIG) + + cover.close_cover(self.hass, COVER_GROUP) + self.hass.block_till_done() + for _ in range(10): + future = dt_util.utcnow() + timedelta(seconds=1) + fire_time_changed(self.hass, future) + self.hass.block_till_done() + + state = self.hass.states.get(COVER_GROUP) + self.assertEqual(state.state, STATE_CLOSED) + self.assertEqual(state.attributes.get(ATTR_CURRENT_POSITION), 0) + + self.assertEqual(self.hass.states.get(DEMO_COVER).state, STATE_CLOSED) + self.assertEqual(self.hass.states.get(DEMO_COVER_POS) + .attributes.get(ATTR_CURRENT_POSITION), 0) + self.assertEqual(self.hass.states.get(DEMO_COVER_TILT) + .attributes.get(ATTR_CURRENT_POSITION), 0) + + def test_stop_covers(self): + """Test stop cover function.""" + with assert_setup_component(2, DOMAIN): + assert setup.setup_component(self.hass, DOMAIN, CONFIG) + + cover.open_cover(self.hass, COVER_GROUP) + self.hass.block_till_done() + future = dt_util.utcnow() + timedelta(seconds=1) + fire_time_changed(self.hass, future) + self.hass.block_till_done() + cover.stop_cover(self.hass, COVER_GROUP) + self.hass.block_till_done() + future = dt_util.utcnow() + timedelta(seconds=1) + fire_time_changed(self.hass, future) + self.hass.block_till_done() + + state = self.hass.states.get(COVER_GROUP) + self.assertEqual(state.state, STATE_OPEN) + self.assertEqual(state.attributes.get(ATTR_CURRENT_POSITION), 100) + + self.assertEqual(self.hass.states.get(DEMO_COVER).state, STATE_OPEN) + self.assertEqual(self.hass.states.get(DEMO_COVER_POS) + .attributes.get(ATTR_CURRENT_POSITION), 20) + self.assertEqual(self.hass.states.get(DEMO_COVER_TILT) + .attributes.get(ATTR_CURRENT_POSITION), 80) + + def test_set_cover_position(self): + """Test set cover position function.""" + with assert_setup_component(2, DOMAIN): + assert setup.setup_component(self.hass, DOMAIN, CONFIG) + + cover.set_cover_position(self.hass, 50, COVER_GROUP) + self.hass.block_till_done() + for _ in range(4): + future = dt_util.utcnow() + timedelta(seconds=1) + fire_time_changed(self.hass, future) + self.hass.block_till_done() + + state = self.hass.states.get(COVER_GROUP) + self.assertEqual(state.state, STATE_OPEN) + self.assertEqual(state.attributes.get(ATTR_CURRENT_POSITION), 50) + + self.assertEqual(self.hass.states.get(DEMO_COVER).state, STATE_CLOSED) + self.assertEqual(self.hass.states.get(DEMO_COVER_POS) + .attributes.get(ATTR_CURRENT_POSITION), 50) + self.assertEqual(self.hass.states.get(DEMO_COVER_TILT) + .attributes.get(ATTR_CURRENT_POSITION), 50) + + def test_open_tilts(self): + """Test open tilt function.""" + with assert_setup_component(2, DOMAIN): + assert setup.setup_component(self.hass, DOMAIN, CONFIG) + + cover.open_cover_tilt(self.hass, COVER_GROUP) + self.hass.block_till_done() + for _ in range(5): + future = dt_util.utcnow() + timedelta(seconds=1) + fire_time_changed(self.hass, future) + self.hass.block_till_done() + + state = self.hass.states.get(COVER_GROUP) + self.assertEqual(state.state, STATE_OPEN) + self.assertEqual(state.attributes.get(ATTR_CURRENT_TILT_POSITION), 100) + + self.assertEqual(self.hass.states.get(DEMO_COVER_TILT) + .attributes.get(ATTR_CURRENT_TILT_POSITION), 100) + + def test_close_tilts(self): + """Test close tilt function.""" + with assert_setup_component(2, DOMAIN): + assert setup.setup_component(self.hass, DOMAIN, CONFIG) + + cover.close_cover_tilt(self.hass, COVER_GROUP) + self.hass.block_till_done() + for _ in range(5): + future = dt_util.utcnow() + timedelta(seconds=1) + fire_time_changed(self.hass, future) + self.hass.block_till_done() + + state = self.hass.states.get(COVER_GROUP) + self.assertEqual(state.state, STATE_OPEN) + self.assertEqual(state.attributes.get(ATTR_CURRENT_TILT_POSITION), 0) + + self.assertEqual(self.hass.states.get(DEMO_COVER_TILT) + .attributes.get(ATTR_CURRENT_TILT_POSITION), 0) + + def test_stop_tilts(self): + """Test stop tilts function.""" + with assert_setup_component(2, DOMAIN): + assert setup.setup_component(self.hass, DOMAIN, CONFIG) + + cover.open_cover_tilt(self.hass, COVER_GROUP) + self.hass.block_till_done() + future = dt_util.utcnow() + timedelta(seconds=1) + fire_time_changed(self.hass, future) + self.hass.block_till_done() + cover.stop_cover_tilt(self.hass, COVER_GROUP) + self.hass.block_till_done() + future = dt_util.utcnow() + timedelta(seconds=1) + fire_time_changed(self.hass, future) + self.hass.block_till_done() + + state = self.hass.states.get(COVER_GROUP) + self.assertEqual(state.state, STATE_OPEN) + self.assertEqual(state.attributes.get(ATTR_CURRENT_TILT_POSITION), 60) + + self.assertEqual(self.hass.states.get(DEMO_COVER_TILT) + .attributes.get(ATTR_CURRENT_TILT_POSITION), 60) + + def test_set_tilt_positions(self): + """Test set tilt position function.""" + with assert_setup_component(2, DOMAIN): + assert setup.setup_component(self.hass, DOMAIN, CONFIG) + + cover.set_cover_tilt_position(self.hass, 80, COVER_GROUP) + self.hass.block_till_done() + for _ in range(3): + future = dt_util.utcnow() + timedelta(seconds=1) + fire_time_changed(self.hass, future) + self.hass.block_till_done() + + state = self.hass.states.get(COVER_GROUP) + self.assertEqual(state.state, STATE_OPEN) + self.assertEqual(state.attributes.get(ATTR_CURRENT_TILT_POSITION), 80) + + self.assertEqual(self.hass.states.get(DEMO_COVER_TILT) + .attributes.get(ATTR_CURRENT_TILT_POSITION), 80) From 5c434f143e875e4d9e9a0aaf416f73cf84d330c6 Mon Sep 17 00:00:00 2001 From: Clement Wong Date: Thu, 15 Mar 2018 13:16:52 +0100 Subject: [PATCH 096/220] Tibber use appNickname as name (#13231) --- homeassistant/components/sensor/tibber.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/tibber.py b/homeassistant/components/sensor/tibber.py index 8c8ffdfd954..435003f76d0 100644 --- a/homeassistant/components/sensor/tibber.py +++ b/homeassistant/components/sensor/tibber.py @@ -61,7 +61,8 @@ class TibberSensor(Entity): self._state = None self._device_state_attributes = {} self._unit_of_measurement = self._tibber_home.price_unit - self._name = 'Electricity price {}'.format(tibber_home.address1) + self._name = 'Electricity price {}'.format(tibber_home.info['viewer'] + ['home']['appNickname']) async def async_update(self): """Get the latest data and updates the states.""" From 92f13ff60d5cb1bd78c5faba4c8f0d8f659a713b Mon Sep 17 00:00:00 2001 From: Eugene Kuzin Date: Thu, 15 Mar 2018 14:43:29 +0200 Subject: [PATCH 097/220] media_content_type attribute display fix (#13204) * media_content_type fix Kodi media_content_type attribute display fix * media_content_type fix (#6989) fixes attribute display for unknown media * code cleanup * trailing whitespaces * comments correction * redundant "else:" removed --- homeassistant/components/media_player/kodi.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/media_player/kodi.py b/homeassistant/components/media_player/kodi.py index 6450b2f5b35..33116258978 100644 --- a/homeassistant/components/media_player/kodi.py +++ b/homeassistant/components/media_player/kodi.py @@ -73,6 +73,8 @@ MEDIA_TYPES = { 'episode': MEDIA_TYPE_TVSHOW, # Type 'channel' is used for radio or tv streams from pvr 'channel': MEDIA_TYPE_CHANNEL, + # Type 'audio' is used for audio media, that Kodi couldn't scroblle + 'audio': MEDIA_TYPE_MUSIC, } SUPPORT_KODI = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ @@ -480,7 +482,12 @@ class KodiDevice(MediaPlayerDevice): @property def media_content_type(self): - """Content type of current playing media.""" + """Content type of current playing media. + + If the media type cannot be detected, the player type is used. + """ + if MEDIA_TYPES.get(self._item.get('type')) is None and self._players: + return MEDIA_TYPES.get(self._players[0]['type']) return MEDIA_TYPES.get(self._item.get('type')) @property From 1d2fd8a2e9c4c89ff3d6155d2447eccdfa71a619 Mon Sep 17 00:00:00 2001 From: Andrei Pop Date: Thu, 15 Mar 2018 17:27:42 +0200 Subject: [PATCH 098/220] Edimax component reports wrong power values (#13011) * Fixed Edimax switch authentication error for newer firmware. * pyedimax moved to pypi * Added pyedimax to gen_requirements_all.py * Cleanup * Fixed https://github.com/home-assistant/home-assistant/issues/13008 * Only ValueError now * Trivial error. --- homeassistant/components/switch/edimax.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/switch/edimax.py b/homeassistant/components/switch/edimax.py index 50b5ba93b85..49eb5d32110 100644 --- a/homeassistant/components/switch/edimax.py +++ b/homeassistant/components/switch/edimax.py @@ -83,14 +83,13 @@ class SmartPlugSwitch(SwitchDevice): def update(self): """Update edimax switch.""" try: - self._now_power = float(self.smartplug.now_power) / 1000000.0 - except (TypeError, ValueError): + self._now_power = float(self.smartplug.now_power) + except ValueError: self._now_power = None try: - self._now_energy_day = (float(self.smartplug.now_energy_day) / - 1000.0) - except (TypeError, ValueError): + self._now_energy_day = float(self.smartplug.now_energy_day) + except ValueError: self._now_energy_day = None self._state = self.smartplug.state == 'ON' From ee6d6a8859434cf7dc9f53570b2412dfc269525c Mon Sep 17 00:00:00 2001 From: Maximilien Cuony Date: Thu, 15 Mar 2018 16:45:27 +0100 Subject: [PATCH 099/220] myStrom: Add RGB support to Wifi bulbs (#13194) --- homeassistant/components/light/mystrom.py | 41 ++++++++++++++++++++--- 1 file changed, 37 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/light/mystrom.py b/homeassistant/components/light/mystrom.py index ecb120e3079..9f049dd2e8a 100644 --- a/homeassistant/components/light/mystrom.py +++ b/homeassistant/components/light/mystrom.py @@ -11,8 +11,10 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.components.light import ( Light, PLATFORM_SCHEMA, ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, - SUPPORT_EFFECT, ATTR_EFFECT, SUPPORT_FLASH) + SUPPORT_EFFECT, ATTR_EFFECT, SUPPORT_FLASH, SUPPORT_RGB_COLOR, + ATTR_RGB_COLOR) from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, STATE_UNKNOWN +from homeassistant.util.color import color_RGB_to_hsv, color_hsv_to_RGB REQUIREMENTS = ['python-mystrom==0.3.8'] @@ -20,7 +22,10 @@ _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = 'myStrom bulb' -SUPPORT_MYSTROM = (SUPPORT_BRIGHTNESS | SUPPORT_EFFECT | SUPPORT_FLASH) +SUPPORT_MYSTROM = ( + SUPPORT_BRIGHTNESS | SUPPORT_EFFECT | SUPPORT_FLASH | + SUPPORT_RGB_COLOR +) EFFECT_RAINBOW = 'rainbow' EFFECT_SUNRISE = 'sunrise' @@ -67,6 +72,8 @@ class MyStromLight(Light): self._state = None self._available = False self._brightness = 0 + self._color_h = 0 + self._color_s = 0 @property def name(self): @@ -83,6 +90,11 @@ class MyStromLight(Light): """Return the brightness of the light.""" return self._brightness + @property + def rgb_color(self): + """Return the color of the light.""" + return color_hsv_to_RGB(self._color_h, self._color_s, self._brightness) + @property def available(self) -> bool: """Return True if entity is available.""" @@ -105,11 +117,25 @@ class MyStromLight(Light): brightness = kwargs.get(ATTR_BRIGHTNESS, 255) effect = kwargs.get(ATTR_EFFECT) + if ATTR_RGB_COLOR in kwargs: + # New color, compute from RGB + color_h, color_s, brightness = color_RGB_to_hsv( + *kwargs[ATTR_RGB_COLOR] + ) + brightness = brightness / 100 * 255 + elif ATTR_BRIGHTNESS in kwargs: + # Brightness update, keep color + color_h, color_s = self._color_h, self._color_s + else: + color_h, color_s = 0, 0 # Back to white + try: if not self.is_on: self._bulb.set_on() if brightness is not None: - self._bulb.set_color_hsv(0, 0, round(brightness * 100 / 255)) + self._bulb.set_color_hsv( + int(color_h), int(color_s), round(brightness * 100 / 255) + ) if effect == EFFECT_SUNRISE: self._bulb.set_sunrise(30) if effect == EFFECT_RAINBOW: @@ -132,7 +158,14 @@ class MyStromLight(Light): try: self._state = self._bulb.get_status() - self._brightness = int(self._bulb.get_brightness()) * 255 / 100 + + colors = self._bulb.get_color()['color'] + color_h, color_s, color_v = colors.split(';') + + self._color_h = int(color_h) + self._color_s = int(color_s) + self._brightness = int(color_v) * 255 / 100 + self._available = True except MyStromConnectionError: _LOGGER.warning("myStrom bulb not online") From 170b8671b9eb0594ca1f628fc7fc0865da010088 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 15 Mar 2018 10:54:22 -0700 Subject: [PATCH 100/220] Fix logbook JSON serialize issue (#13229) * Fix logbook JSON serialize issue * Address flakiness * Lint * deflake ? * Deflake 2 --- homeassistant/components/logbook.py | 9 ++++++--- homeassistant/components/recorder/__init__.py | 13 +++++-------- tests/components/test_history.py | 6 ++++-- tests/components/test_logbook.py | 16 ++++++++++++++-- 4 files changed, 29 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/logbook.py b/homeassistant/components/logbook.py index d0b944793c4..1c3e8ed1f19 100644 --- a/homeassistant/components/logbook.py +++ b/homeassistant/components/logbook.py @@ -139,9 +139,12 @@ class LogbookView(HomeAssistantView): end_day = start_day + timedelta(days=1) hass = request.app['hass'] - events = yield from hass.async_add_job( - _get_events, hass, self.config, start_day, end_day) - response = yield from hass.async_add_job(self.json, events) + def json_events(): + """Fetch events and generate JSON.""" + return self.json(list( + _get_events(hass, self.config, start_day, end_day))) + + response = yield from hass.async_add_job(json_events) return response diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index 392bccb56d4..23c073ff80a 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -29,6 +29,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entityfilter import generate_filter from homeassistant.helpers.typing import ConfigType import homeassistant.util.dt as dt_util +from homeassistant.loader import bind_hass from . import migration, purge from .const import DATA_INSTANCE @@ -87,14 +88,10 @@ CONFIG_SCHEMA = vol.Schema({ }, extra=vol.ALLOW_EXTRA) -@asyncio.coroutine -def wait_connection_ready(hass): - """ - Wait till the connection is ready. - - Returns a coroutine object. - """ - return (yield from hass.data[DATA_INSTANCE].async_db_ready) +@bind_hass +async def wait_connection_ready(hass): + """Wait till the connection is ready.""" + return await hass.data[DATA_INSTANCE].async_db_ready def run_information(hass, point_in_time: Optional[datetime] = None): diff --git a/tests/components/test_history.py b/tests/components/test_history.py index be768f5ec69..bea2af396cb 100644 --- a/tests/components/test_history.py +++ b/tests/components/test_history.py @@ -483,11 +483,13 @@ class TestComponentHistory(unittest.TestCase): return zero, four, states -async def test_fetch_period_api(hass, test_client): +async def test_fetch_period_api(hass, aiohttp_client): """Test the fetch period view for history.""" await hass.async_add_job(init_recorder_component, hass) await async_setup_component(hass, 'history', {}) - client = await test_client(hass.http.app) + await hass.components.recorder.wait_connection_ready() + await hass.async_add_job(hass.data[recorder.DATA_INSTANCE].block_till_done) + client = await aiohttp_client(hass.http.app) response = await client.get( '/api/history/period/{}'.format(dt_util.utcnow().isoformat())) assert response.status == 200 diff --git a/tests/components/test_logbook.py b/tests/components/test_logbook.py index bd10416c7a2..6c71a263afa 100644 --- a/tests/components/test_logbook.py +++ b/tests/components/test_logbook.py @@ -10,8 +10,8 @@ from homeassistant.const import ( EVENT_STATE_CHANGED, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, ATTR_HIDDEN, STATE_NOT_HOME, STATE_ON, STATE_OFF) import homeassistant.util.dt as dt_util -from homeassistant.components import logbook -from homeassistant.setup import setup_component +from homeassistant.components import logbook, recorder +from homeassistant.setup import setup_component, async_setup_component from tests.common import ( init_recorder_component, get_test_home_assistant) @@ -555,3 +555,15 @@ class TestComponentLogbook(unittest.TestCase): 'old_state': state, 'new_state': state, }, time_fired=event_time_fired) + + +async def test_logbook_view(hass, aiohttp_client): + """Test the logbook view.""" + await hass.async_add_job(init_recorder_component, hass) + await async_setup_component(hass, 'logbook', {}) + await hass.components.recorder.wait_connection_ready() + await hass.async_add_job(hass.data[recorder.DATA_INSTANCE].block_till_done) + client = await aiohttp_client(hass.http.app) + response = await client.get( + '/api/logbook/{}'.format(dt_util.utcnow().isoformat())) + assert response.status == 200 From ff416c0e7ab8861dcb1356d1730ccd46e29391b4 Mon Sep 17 00:00:00 2001 From: maxlaverse Date: Thu, 15 Mar 2018 18:58:11 +0100 Subject: [PATCH 101/220] Try to fix caldav (#13236) * Fix device attribute type for event end * Fix is_over and add tests --- homeassistant/components/calendar/caldav.py | 6 ++- tests/components/calendar/test_caldav.py | 54 ++++++++++++++++++++- 2 files changed, 57 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/calendar/caldav.py b/homeassistant/components/calendar/caldav.py index d70e7ff8946..6f92891c551 100644 --- a/homeassistant/components/calendar/caldav.py +++ b/homeassistant/components/calendar/caldav.py @@ -194,7 +194,9 @@ class WebDavCalendarData(object): @staticmethod def is_over(vevent): """Return if the event is over.""" - return dt.now() > WebDavCalendarData.get_end_date(vevent) + return dt.now() >= WebDavCalendarData.to_datetime( + WebDavCalendarData.get_end_date(vevent) + ) @staticmethod def get_hass_date(obj): @@ -230,4 +232,4 @@ class WebDavCalendarData(object): else: enddate = obj.dtstart.value + timedelta(days=1) - return WebDavCalendarData.to_datetime(enddate) + return enddate diff --git a/tests/components/calendar/test_caldav.py b/tests/components/calendar/test_caldav.py index e44e5cfc1f0..11dd0cb9635 100644 --- a/tests/components/calendar/test_caldav.py +++ b/tests/components/calendar/test_caldav.py @@ -105,6 +105,20 @@ LOCATION:Hamburg DESCRIPTION:What a day END:VEVENT END:VCALENDAR +""", + """BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Global Corp.//CalDAV Client//EN +BEGIN:VEVENT +UID:7 +DTSTART;TZID=America/Los_Angeles:20171127T083000 +DTSTAMP:20180301T020053Z +DTEND;TZID=America/Los_Angeles:20171127T093000 +SUMMARY:Enjoy the sun +LOCATION:San Francisco +DESCRIPTION:Sunny day +END:VEVENT +END:VCALENDAR """ ] @@ -225,7 +239,7 @@ class TestComponentsWebDavCalendar(unittest.TestCase): }, _add_device) - @patch('homeassistant.util.dt.now', return_value=_local_datetime(17, 30)) + @patch('homeassistant.util.dt.now', return_value=_local_datetime(17, 45)) def test_ongoing_event(self, mock_now): """Test that the ongoing event is returned.""" cal = caldav.WebDavCalendarEventDevice(self.hass, @@ -244,6 +258,44 @@ class TestComponentsWebDavCalendar(unittest.TestCase): "description": "Surprisingly rainy" }) + @patch('homeassistant.util.dt.now', return_value=_local_datetime(17, 30)) + def test_just_ended_event(self, mock_now): + """Test that the next ongoing event is returned.""" + cal = caldav.WebDavCalendarEventDevice(self.hass, + DEVICE_DATA, + self.calendar) + + self.assertEqual(cal.name, DEVICE_DATA["name"]) + self.assertEqual(cal.state, STATE_ON) + self.assertEqual(cal.device_state_attributes, { + "message": "This is a normal event", + "all_day": False, + "offset_reached": False, + "start_time": "2017-11-27 17:00:00", + "end_time": "2017-11-27 18:00:00", + "location": "Hamburg", + "description": "Surprisingly rainy" + }) + + @patch('homeassistant.util.dt.now', return_value=_local_datetime(17, 00)) + def test_ongoing_event_different_tz(self, mock_now): + """Test that the ongoing event with another timezone is returned.""" + cal = caldav.WebDavCalendarEventDevice(self.hass, + DEVICE_DATA, + self.calendar) + + self.assertEqual(cal.name, DEVICE_DATA["name"]) + self.assertEqual(cal.state, STATE_ON) + self.assertEqual(cal.device_state_attributes, { + "message": "Enjoy the sun", + "all_day": False, + "offset_reached": False, + "start_time": "2017-11-27 16:30:00", + "description": "Sunny day", + "end_time": "2017-11-27 17:30:00", + "location": "San Francisco" + }) + @patch('homeassistant.util.dt.now', return_value=_local_datetime(8, 30)) def test_ongoing_event_with_offset(self, mock_now): """Test that the offset is taken into account.""" From 5e675677ad26b9e8093faf6fb23156b09ae08951 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Thu, 15 Mar 2018 20:43:28 +0100 Subject: [PATCH 102/220] Cleanup Sonos platform setup (#13225) * Cleanup Sonos platform setup * Remove unneeded lists --- .../components/media_player/sonos.py | 28 ++++++-------- tests/components/media_player/test_sonos.py | 38 ++++--------------- 2 files changed, 19 insertions(+), 47 deletions(-) diff --git a/homeassistant/components/media_player/sonos.py b/homeassistant/components/media_player/sonos.py index 2a12b59e7c7..e124fbd0443 100644 --- a/homeassistant/components/media_player/sonos.py +++ b/homeassistant/components/media_player/sonos.py @@ -54,7 +54,6 @@ DATA_SONOS = 'sonos' SOURCE_LINEIN = 'Line-in' SOURCE_TV = 'TV' -CONF_ADVERTISE_ADDR = 'advertise_addr' CONF_INTERFACE_ADDR = 'interface_addr' # Service call validation schemas @@ -73,7 +72,6 @@ ATTR_IS_COORDINATOR = 'is_coordinator' UPNP_ERRORS_TO_IGNORE = ['701', '711'] PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_ADVERTISE_ADDR): cv.string, vol.Optional(CONF_INTERFACE_ADDR): cv.string, vol.Optional(CONF_HOSTS): vol.All(cv.ensure_list, [cv.string]), }) @@ -141,10 +139,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): if DATA_SONOS not in hass.data: hass.data[DATA_SONOS] = SonosData() - advertise_addr = config.get(CONF_ADVERTISE_ADDR, None) - if advertise_addr: - soco.config.EVENT_ADVERTISE_IP = advertise_addr - + players = [] if discovery_info: player = soco.SoCo(discovery_info.get('host')) @@ -152,25 +147,24 @@ def setup_platform(hass, config, add_devices, discovery_info=None): if player.uid in hass.data[DATA_SONOS].uids: return - if player.is_visible: - hass.data[DATA_SONOS].uids.add(player.uid) - add_devices([SonosDevice(player)]) + # If invisible, such as a stereo slave + if not player.is_visible: + return + + players.append(player) else: - players = None - hosts = config.get(CONF_HOSTS, None) + hosts = config.get(CONF_HOSTS) if hosts: # Support retro compatibility with comma separated list of hosts # from config hosts = hosts[0] if len(hosts) == 1 else hosts hosts = hosts.split(',') if isinstance(hosts, str) else hosts - players = [] for host in hosts: try: players.append(soco.SoCo(socket.gethostbyname(host))) except OSError: _LOGGER.warning("Failed to initialize '%s'", host) - - if not players: + else: players = soco.discover( interface_addr=config.get(CONF_INTERFACE_ADDR)) @@ -178,9 +172,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None): _LOGGER.warning("No Sonos speakers found") return - hass.data[DATA_SONOS].uids.update([p.uid for p in players]) - add_devices([SonosDevice(p) for p in players]) - _LOGGER.debug("Added %s Sonos speakers", len(players)) + hass.data[DATA_SONOS].uids.update(p.uid for p in players) + add_devices(SonosDevice(p) for p in players) + _LOGGER.debug("Added %s Sonos speakers", len(players)) def service_handle(service): """Handle for services.""" diff --git a/tests/components/media_player/test_sonos.py b/tests/components/media_player/test_sonos.py index 3470c79ad64..f741898d15e 100644 --- a/tests/components/media_player/test_sonos.py +++ b/tests/components/media_player/test_sonos.py @@ -9,8 +9,7 @@ from soco import alarms from homeassistant.setup import setup_component from homeassistant.components.media_player import sonos, DOMAIN -from homeassistant.components.media_player.sonos import CONF_INTERFACE_ADDR, \ - CONF_ADVERTISE_ADDR +from homeassistant.components.media_player.sonos import CONF_INTERFACE_ADDR from homeassistant.const import CONF_HOSTS, CONF_PLATFORM from tests.common import get_test_home_assistant @@ -162,7 +161,7 @@ class TestSonosMediaPlayer(unittest.TestCase): 'host': '192.0.2.1' }) - devices = self.hass.data[sonos.DATA_SONOS].devices + devices = list(self.hass.data[sonos.DATA_SONOS].devices) self.assertEqual(len(devices), 1) self.assertEqual(devices[0].name, 'Kitchen') @@ -185,27 +184,6 @@ class TestSonosMediaPlayer(unittest.TestCase): self.assertEqual(len(self.hass.data[sonos.DATA_SONOS].devices), 1) self.assertEqual(discover_mock.call_count, 1) - @mock.patch('soco.SoCo', new=SoCoMock) - @mock.patch('socket.create_connection', side_effect=socket.error()) - @mock.patch('soco.discover') - def test_ensure_setup_config_advertise_addr(self, discover_mock, - *args): - """Test an advertise address config'd by the HASS config file.""" - discover_mock.return_value = {SoCoMock('192.0.2.1')} - - config = { - DOMAIN: { - CONF_PLATFORM: 'sonos', - CONF_ADVERTISE_ADDR: '192.0.1.1', - } - } - - assert setup_component(self.hass, DOMAIN, config) - - self.assertEqual(len(self.hass.data[sonos.DATA_SONOS].devices), 1) - self.assertEqual(discover_mock.call_count, 1) - self.assertEqual(soco.config.EVENT_ADVERTISE_IP, '192.0.1.1') - @mock.patch('soco.SoCo', new=SoCoMock) @mock.patch('socket.create_connection', side_effect=socket.error()) def test_ensure_setup_config_hosts_string_single(self, *args): @@ -263,7 +241,7 @@ class TestSonosMediaPlayer(unittest.TestCase): def test_ensure_setup_sonos_discovery(self, *args): """Test a single device using the autodiscovery provided by Sonos.""" sonos.setup_platform(self.hass, {}, add_devices_factory(self.hass)) - devices = self.hass.data[sonos.DATA_SONOS].devices + devices = list(self.hass.data[sonos.DATA_SONOS].devices) self.assertEqual(len(devices), 1) self.assertEqual(devices[0].name, 'Kitchen') @@ -275,7 +253,7 @@ class TestSonosMediaPlayer(unittest.TestCase): sonos.setup_platform(self.hass, {}, add_devices_factory(self.hass), { 'host': '192.0.2.1' }) - device = self.hass.data[sonos.DATA_SONOS].devices[-1] + device = list(self.hass.data[sonos.DATA_SONOS].devices)[-1] device.hass = self.hass device.set_sleep_timer(30) @@ -289,7 +267,7 @@ class TestSonosMediaPlayer(unittest.TestCase): sonos.setup_platform(self.hass, {}, add_devices_factory(self.hass), { 'host': '192.0.2.1' }) - device = self.hass.data[sonos.DATA_SONOS].devices[-1] + device = list(self.hass.data[sonos.DATA_SONOS].devices)[-1] device.hass = self.hass device.set_sleep_timer(None) @@ -303,7 +281,7 @@ class TestSonosMediaPlayer(unittest.TestCase): sonos.setup_platform(self.hass, {}, add_devices_factory(self.hass), { 'host': '192.0.2.1' }) - device = self.hass.data[sonos.DATA_SONOS].devices[-1] + device = list(self.hass.data[sonos.DATA_SONOS].devices)[-1] device.hass = self.hass alarm1 = alarms.Alarm(soco_mock) alarm1.configure_mock(_alarm_id="1", start_time=None, enabled=False, @@ -333,7 +311,7 @@ class TestSonosMediaPlayer(unittest.TestCase): sonos.setup_platform(self.hass, {}, add_devices_factory(self.hass), { 'host': '192.0.2.1' }) - device = self.hass.data[sonos.DATA_SONOS].devices[-1] + device = list(self.hass.data[sonos.DATA_SONOS].devices)[-1] device.hass = self.hass snapshotMock.return_value = True @@ -351,7 +329,7 @@ class TestSonosMediaPlayer(unittest.TestCase): sonos.setup_platform(self.hass, {}, add_devices_factory(self.hass), { 'host': '192.0.2.1' }) - device = self.hass.data[sonos.DATA_SONOS].devices[-1] + device = list(self.hass.data[sonos.DATA_SONOS].devices)[-1] device.hass = self.hass restoreMock.return_value = True From a86bf81768bc050f49557be4d2da8d9a01ac3f93 Mon Sep 17 00:00:00 2001 From: Juggels Date: Thu, 15 Mar 2018 21:43:20 +0100 Subject: [PATCH 103/220] Fix 'dict' object has no attribute 'strftime' (#13215) * Fix 'dict' object has no attribute 'strftime' * Clear existing list instead of new object --- homeassistant/components/calendar/todoist.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/calendar/todoist.py b/homeassistant/components/calendar/todoist.py index c5ae1dd3c11..02840c7d0ee 100644 --- a/homeassistant/components/calendar/todoist.py +++ b/homeassistant/components/calendar/todoist.py @@ -496,6 +496,10 @@ class TodoistProjectData(object): # We had no valid tasks return True + # Make sure the task collection is reset to prevent an + # infinite collection repeating the same tasks + self.all_project_tasks.clear() + # Organize the best tasks (so users can see all the tasks # they have, organized) while project_tasks: From 89a19c89a768988a35effe4c0463c3a639304a60 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 15 Mar 2018 13:49:49 -0700 Subject: [PATCH 104/220] Fix aiohttp deprecation warnings (#13240) * Fix aiohttp deprecation warnings * Fix Ring deprecation warning * Lint --- homeassistant/components/ring.py | 4 +- homeassistant/helpers/aiohttp_client.py | 41 ++++++--------- .../components/alexa/test_flash_briefings.py | 4 +- tests/components/alexa/test_intent.py | 4 +- tests/components/alexa/test_smart_home.py | 12 ++--- tests/components/camera/test_generic.py | 12 ++--- tests/components/camera/test_local_file.py | 8 +-- tests/components/camera/test_mqtt.py | 4 +- tests/components/cloud/test_http_api.py | 4 +- .../components/config/test_config_entries.py | 4 +- tests/components/config/test_core.py | 4 +- tests/components/config/test_customize.py | 16 +++--- .../components/config/test_entity_registry.py | 4 +- tests/components/config/test_group.py | 20 +++---- tests/components/config/test_hassbian.py | 8 +-- tests/components/config/test_init.py | 4 +- tests/components/config/test_zwave.py | 4 +- .../device_tracker/test_geofency.py | 4 +- .../device_tracker/test_locative.py | 4 +- .../components/device_tracker/test_meraki.py | 4 +- .../device_tracker/test_owntracks_http.py | 4 +- tests/components/emulated_hue/test_hue_api.py | 4 +- .../google_assistant/test_google_assistant.py | 4 +- tests/components/hassio/conftest.py | 4 +- tests/components/http/test_auth.py | 20 +++---- tests/components/http/test_ban.py | 8 +-- tests/components/http/test_cors.py | 4 +- tests/components/http/test_data_validator.py | 12 ++--- tests/components/http/test_init.py | 10 ++-- tests/components/http/test_real_ip.py | 8 +-- tests/components/mailbox/test_init.py | 4 +- tests/components/mqtt/test_init.py | 4 +- tests/components/notify/test_html5.py | 51 +++++++++--------- tests/components/sensor/test_mhz19.py | 6 +-- tests/components/test_api.py | 4 +- tests/components/test_conversation.py | 12 ++--- tests/components/test_frontend.py | 12 ++--- tests/components/test_prometheus.py | 6 +-- tests/components/test_ring.py | 5 +- tests/components/test_rss_feed_template.py | 4 +- tests/components/test_shopping_list.py | 24 ++++----- tests/components/test_system_log.py | 52 +++++++++---------- tests/components/test_websocket_api.py | 8 +-- tests/helpers/test_aiohttp_client.py | 4 +- tests/test_util/aiohttp.py | 3 +- 45 files changed, 221 insertions(+), 225 deletions(-) diff --git a/homeassistant/components/ring.py b/homeassistant/components/ring.py index 6e70ddb244d..1a15e22fca0 100644 --- a/homeassistant/components/ring.py +++ b/homeassistant/components/ring.py @@ -37,8 +37,8 @@ CONFIG_SCHEMA = vol.Schema({ def setup(hass, config): """Set up the Ring component.""" conf = config[DOMAIN] - username = conf.get(CONF_USERNAME) - password = conf.get(CONF_PASSWORD) + username = conf[CONF_USERNAME] + password = conf[CONF_PASSWORD] try: from ring_doorbell import Ring diff --git a/homeassistant/helpers/aiohttp_client.py b/homeassistant/helpers/aiohttp_client.py index 72f2214b5e7..bb34942ad79 100644 --- a/homeassistant/helpers/aiohttp_client.py +++ b/homeassistant/helpers/aiohttp_client.py @@ -149,34 +149,27 @@ def _async_get_connector(hass, verify_ssl=True): This method must be run in the event loop. """ - is_new = False + key = DATA_CONNECTOR if verify_ssl else DATA_CONNECTOR_NOTVERIFY + + if key in hass.data: + return hass.data[key] if verify_ssl: - if DATA_CONNECTOR not in hass.data: - ssl_context = ssl.SSLContext(ssl.PROTOCOL_SSLv23) - ssl_context.load_verify_locations(cafile=certifi.where(), - capath=None) - connector = aiohttp.TCPConnector(loop=hass.loop, - ssl_context=ssl_context) - hass.data[DATA_CONNECTOR] = connector - is_new = True - else: - connector = hass.data[DATA_CONNECTOR] + ssl_context = ssl.SSLContext(ssl.PROTOCOL_SSLv23) + ssl_context.load_verify_locations(cafile=certifi.where(), + capath=None) else: - if DATA_CONNECTOR_NOTVERIFY not in hass.data: - connector = aiohttp.TCPConnector(loop=hass.loop, verify_ssl=False) - hass.data[DATA_CONNECTOR_NOTVERIFY] = connector - is_new = True - else: - connector = hass.data[DATA_CONNECTOR_NOTVERIFY] + ssl_context = False - if is_new: - @callback - def _async_close_connector(event): - """Close connector pool.""" - connector.close() + connector = aiohttp.TCPConnector(loop=hass.loop, ssl=ssl_context) + hass.data[key] = connector - hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_CLOSE, _async_close_connector) + @callback + def _async_close_connector(event): + """Close connector pool.""" + connector.close() + + hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_CLOSE, _async_close_connector) return connector diff --git a/tests/components/alexa/test_flash_briefings.py b/tests/components/alexa/test_flash_briefings.py index d9f0c8e156d..d7871e82afc 100644 --- a/tests/components/alexa/test_flash_briefings.py +++ b/tests/components/alexa/test_flash_briefings.py @@ -21,7 +21,7 @@ NPR_NEWS_MP3_URL = "https://pd.npr.org/anon.npr-mp3/npr/news/newscast.mp3" @pytest.fixture -def alexa_client(loop, hass, test_client): +def alexa_client(loop, hass, aiohttp_client): """Initialize a Home Assistant server for testing this module.""" @callback def mock_service(call): @@ -49,7 +49,7 @@ def alexa_client(loop, hass, test_client): }, } })) - return loop.run_until_complete(test_client(hass.http.app)) + return loop.run_until_complete(aiohttp_client(hass.http.app)) def _flash_briefing_req(client, briefing_id): diff --git a/tests/components/alexa/test_intent.py b/tests/components/alexa/test_intent.py index 2c8fafde155..d15c7ccbb34 100644 --- a/tests/components/alexa/test_intent.py +++ b/tests/components/alexa/test_intent.py @@ -23,7 +23,7 @@ NPR_NEWS_MP3_URL = "https://pd.npr.org/anon.npr-mp3/npr/news/newscast.mp3" @pytest.fixture -def alexa_client(loop, hass, test_client): +def alexa_client(loop, hass, aiohttp_client): """Initialize a Home Assistant server for testing this module.""" @callback def mock_service(call): @@ -95,7 +95,7 @@ def alexa_client(loop, hass, test_client): }, } })) - return loop.run_until_complete(test_client(hass.http.app)) + return loop.run_until_complete(aiohttp_client(hass.http.app)) def _intent_req(client, data=None): diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index 8de4d0d9aff..a5375ba2662 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -1199,10 +1199,10 @@ def test_unsupported_domain(hass): @asyncio.coroutine -def do_http_discovery(config, hass, test_client): +def do_http_discovery(config, hass, aiohttp_client): """Submit a request to the Smart Home HTTP API.""" yield from async_setup_component(hass, alexa.DOMAIN, config) - http_client = yield from test_client(hass.http.app) + http_client = yield from aiohttp_client(hass.http.app) request = get_new_request('Alexa.Discovery', 'Discover') response = yield from http_client.post( @@ -1213,7 +1213,7 @@ def do_http_discovery(config, hass, test_client): @asyncio.coroutine -def test_http_api(hass, test_client): +def test_http_api(hass, aiohttp_client): """With `smart_home:` HTTP API is exposed.""" config = { 'alexa': { @@ -1221,7 +1221,7 @@ def test_http_api(hass, test_client): } } - response = yield from do_http_discovery(config, hass, test_client) + response = yield from do_http_discovery(config, hass, aiohttp_client) response_data = yield from response.json() # Here we're testing just the HTTP view glue -- details of discovery are @@ -1230,12 +1230,12 @@ def test_http_api(hass, test_client): @asyncio.coroutine -def test_http_api_disabled(hass, test_client): +def test_http_api_disabled(hass, aiohttp_client): """Without `smart_home:`, the HTTP API is disabled.""" config = { 'alexa': {} } - response = yield from do_http_discovery(config, hass, test_client) + response = yield from do_http_discovery(config, hass, aiohttp_client) assert response.status == 404 diff --git a/tests/components/camera/test_generic.py b/tests/components/camera/test_generic.py index 84eaf107d70..01edca1e996 100644 --- a/tests/components/camera/test_generic.py +++ b/tests/components/camera/test_generic.py @@ -6,7 +6,7 @@ from homeassistant.setup import async_setup_component @asyncio.coroutine -def test_fetching_url(aioclient_mock, hass, test_client): +def test_fetching_url(aioclient_mock, hass, aiohttp_client): """Test that it fetches the given url.""" aioclient_mock.get('http://example.com', text='hello world') @@ -19,7 +19,7 @@ def test_fetching_url(aioclient_mock, hass, test_client): 'password': 'pass' }}) - client = yield from test_client(hass.http.app) + client = yield from aiohttp_client(hass.http.app) resp = yield from client.get('/api/camera_proxy/camera.config_test') @@ -33,7 +33,7 @@ def test_fetching_url(aioclient_mock, hass, test_client): @asyncio.coroutine -def test_limit_refetch(aioclient_mock, hass, test_client): +def test_limit_refetch(aioclient_mock, hass, aiohttp_client): """Test that it fetches the given url.""" aioclient_mock.get('http://example.com/5a', text='hello world') aioclient_mock.get('http://example.com/10a', text='hello world') @@ -49,7 +49,7 @@ def test_limit_refetch(aioclient_mock, hass, test_client): 'limit_refetch_to_url_change': True, }}) - client = yield from test_client(hass.http.app) + client = yield from aiohttp_client(hass.http.app) resp = yield from client.get('/api/camera_proxy/camera.config_test') @@ -94,7 +94,7 @@ def test_limit_refetch(aioclient_mock, hass, test_client): @asyncio.coroutine -def test_camera_content_type(aioclient_mock, hass, test_client): +def test_camera_content_type(aioclient_mock, hass, aiohttp_client): """Test generic camera with custom content_type.""" svg_image = '' urlsvg = 'https://upload.wikimedia.org/wikipedia/commons/0/02/SVG_logo.svg' @@ -113,7 +113,7 @@ def test_camera_content_type(aioclient_mock, hass, test_client): yield from async_setup_component(hass, 'camera', { 'camera': [cam_config_svg, cam_config_normal]}) - client = yield from test_client(hass.http.app) + client = yield from aiohttp_client(hass.http.app) resp_1 = yield from client.get('/api/camera_proxy/camera.config_test_svg') assert aioclient_mock.call_count == 1 diff --git a/tests/components/camera/test_local_file.py b/tests/components/camera/test_local_file.py index 42ce7bd7add..1098c8c9233 100644 --- a/tests/components/camera/test_local_file.py +++ b/tests/components/camera/test_local_file.py @@ -12,7 +12,7 @@ from tests.common import mock_registry @asyncio.coroutine -def test_loading_file(hass, test_client): +def test_loading_file(hass, aiohttp_client): """Test that it loads image from disk.""" mock_registry(hass) @@ -25,7 +25,7 @@ def test_loading_file(hass, test_client): 'file_path': 'mock.file', }}) - client = yield from test_client(hass.http.app) + client = yield from aiohttp_client(hass.http.app) m_open = MockOpen(read_data=b'hello') with mock.patch( @@ -57,7 +57,7 @@ def test_file_not_readable(hass, caplog): @asyncio.coroutine -def test_camera_content_type(hass, test_client): +def test_camera_content_type(hass, aiohttp_client): """Test local_file camera content_type.""" cam_config_jpg = { 'name': 'test_jpg', @@ -84,7 +84,7 @@ def test_camera_content_type(hass, test_client): 'camera': [cam_config_jpg, cam_config_png, cam_config_svg, cam_config_noext]}) - client = yield from test_client(hass.http.app) + client = yield from aiohttp_client(hass.http.app) image = 'hello' m_open = MockOpen(read_data=image.encode()) diff --git a/tests/components/camera/test_mqtt.py b/tests/components/camera/test_mqtt.py index 20d15efd982..d83054d7732 100644 --- a/tests/components/camera/test_mqtt.py +++ b/tests/components/camera/test_mqtt.py @@ -8,7 +8,7 @@ from tests.common import ( @asyncio.coroutine -def test_run_camera_setup(hass, test_client): +def test_run_camera_setup(hass, aiohttp_client): """Test that it fetches the given payload.""" topic = 'test/camera' yield from async_mock_mqtt_component(hass) @@ -24,7 +24,7 @@ def test_run_camera_setup(hass, test_client): async_fire_mqtt_message(hass, topic, 'beer') yield from hass.async_block_till_done() - client = yield from test_client(hass.http.app) + client = yield from aiohttp_client(hass.http.app) resp = yield from client.get(url) assert resp.status == 200 body = yield from resp.text() diff --git a/tests/components/cloud/test_http_api.py b/tests/components/cloud/test_http_api.py index 98ddebb5db3..1ed3d1b4744 100644 --- a/tests/components/cloud/test_http_api.py +++ b/tests/components/cloud/test_http_api.py @@ -12,7 +12,7 @@ from tests.common import mock_coro @pytest.fixture -def cloud_client(hass, test_client): +def cloud_client(hass, aiohttp_client): """Fixture that can fetch from the cloud client.""" with patch('homeassistant.components.cloud.Cloud.async_start', return_value=mock_coro()): @@ -28,7 +28,7 @@ def cloud_client(hass, test_client): hass.data['cloud']._decode_claims = \ lambda token: jwt.get_unverified_claims(token) with patch('homeassistant.components.cloud.Cloud.write_user_info'): - yield hass.loop.run_until_complete(test_client(hass.http.app)) + yield hass.loop.run_until_complete(aiohttp_client(hass.http.app)) @pytest.fixture diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index 84667b8704b..cfe6b12baac 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -17,11 +17,11 @@ from tests.common import MockConfigEntry, MockModule, mock_coro_func @pytest.fixture -def client(hass, test_client): +def client(hass, aiohttp_client): """Fixture that can interact with the config manager API.""" hass.loop.run_until_complete(async_setup_component(hass, 'http', {})) hass.loop.run_until_complete(config_entries.async_setup(hass)) - yield hass.loop.run_until_complete(test_client(hass.http.app)) + yield hass.loop.run_until_complete(aiohttp_client(hass.http.app)) @asyncio.coroutine diff --git a/tests/components/config/test_core.py b/tests/components/config/test_core.py index 4d82d695f8b..5b52b3d5711 100644 --- a/tests/components/config/test_core.py +++ b/tests/components/config/test_core.py @@ -8,14 +8,14 @@ from tests.common import mock_coro @asyncio.coroutine -def test_validate_config_ok(hass, test_client): +def test_validate_config_ok(hass, aiohttp_client): """Test checking config.""" with patch.object(config, 'SECTIONS', ['core']): yield from async_setup_component(hass, 'config', {}) yield from asyncio.sleep(0.1, loop=hass.loop) - client = yield from test_client(hass.http.app) + client = yield from aiohttp_client(hass.http.app) with patch( 'homeassistant.components.config.core.async_check_ha_config_file', diff --git a/tests/components/config/test_customize.py b/tests/components/config/test_customize.py index f12774c25d9..100a18618e6 100644 --- a/tests/components/config/test_customize.py +++ b/tests/components/config/test_customize.py @@ -9,12 +9,12 @@ from homeassistant.config import DATA_CUSTOMIZE @asyncio.coroutine -def test_get_entity(hass, test_client): +def test_get_entity(hass, aiohttp_client): """Test getting entity.""" with patch.object(config, 'SECTIONS', ['customize']): yield from async_setup_component(hass, 'config', {}) - client = yield from test_client(hass.http.app) + client = yield from aiohttp_client(hass.http.app) def mock_read(path): """Mock reading data.""" @@ -38,12 +38,12 @@ def test_get_entity(hass, test_client): @asyncio.coroutine -def test_update_entity(hass, test_client): +def test_update_entity(hass, aiohttp_client): """Test updating entity.""" with patch.object(config, 'SECTIONS', ['customize']): yield from async_setup_component(hass, 'config', {}) - client = yield from test_client(hass.http.app) + client = yield from aiohttp_client(hass.http.app) orig_data = { 'hello.beer': { @@ -89,12 +89,12 @@ def test_update_entity(hass, test_client): @asyncio.coroutine -def test_update_entity_invalid_key(hass, test_client): +def test_update_entity_invalid_key(hass, aiohttp_client): """Test updating entity.""" with patch.object(config, 'SECTIONS', ['customize']): yield from async_setup_component(hass, 'config', {}) - client = yield from test_client(hass.http.app) + client = yield from aiohttp_client(hass.http.app) resp = yield from client.post( '/api/config/customize/config/not_entity', data=json.dumps({ @@ -105,12 +105,12 @@ def test_update_entity_invalid_key(hass, test_client): @asyncio.coroutine -def test_update_entity_invalid_json(hass, test_client): +def test_update_entity_invalid_json(hass, aiohttp_client): """Test updating entity.""" with patch.object(config, 'SECTIONS', ['customize']): yield from async_setup_component(hass, 'config', {}) - client = yield from test_client(hass.http.app) + client = yield from aiohttp_client(hass.http.app) resp = yield from client.post( '/api/config/customize/config/hello.beer', data='not json') diff --git a/tests/components/config/test_entity_registry.py b/tests/components/config/test_entity_registry.py index aa7a5ce5f0e..fd7c6999477 100644 --- a/tests/components/config/test_entity_registry.py +++ b/tests/components/config/test_entity_registry.py @@ -8,11 +8,11 @@ from tests.common import mock_registry, MockEntity, MockEntityPlatform @pytest.fixture -def client(hass, test_client): +def client(hass, aiohttp_client): """Fixture that can interact with the config manager API.""" hass.loop.run_until_complete(async_setup_component(hass, 'http', {})) hass.loop.run_until_complete(entity_registry.async_setup(hass)) - yield hass.loop.run_until_complete(test_client(hass.http.app)) + yield hass.loop.run_until_complete(aiohttp_client(hass.http.app)) async def test_get_entity(hass, client): diff --git a/tests/components/config/test_group.py b/tests/components/config/test_group.py index ad28b6eb9b8..06ba2ff1105 100644 --- a/tests/components/config/test_group.py +++ b/tests/components/config/test_group.py @@ -11,12 +11,12 @@ VIEW_NAME = 'api:config:group:config' @asyncio.coroutine -def test_get_device_config(hass, test_client): +def test_get_device_config(hass, aiohttp_client): """Test getting device config.""" with patch.object(config, 'SECTIONS', ['group']): yield from async_setup_component(hass, 'config', {}) - client = yield from test_client(hass.http.app) + client = yield from aiohttp_client(hass.http.app) def mock_read(path): """Mock reading data.""" @@ -40,12 +40,12 @@ def test_get_device_config(hass, test_client): @asyncio.coroutine -def test_update_device_config(hass, test_client): +def test_update_device_config(hass, aiohttp_client): """Test updating device config.""" with patch.object(config, 'SECTIONS', ['group']): yield from async_setup_component(hass, 'config', {}) - client = yield from test_client(hass.http.app) + client = yield from aiohttp_client(hass.http.app) orig_data = { 'hello.beer': { @@ -89,12 +89,12 @@ def test_update_device_config(hass, test_client): @asyncio.coroutine -def test_update_device_config_invalid_key(hass, test_client): +def test_update_device_config_invalid_key(hass, aiohttp_client): """Test updating device config.""" with patch.object(config, 'SECTIONS', ['group']): yield from async_setup_component(hass, 'config', {}) - client = yield from test_client(hass.http.app) + client = yield from aiohttp_client(hass.http.app) resp = yield from client.post( '/api/config/group/config/not a slug', data=json.dumps({ @@ -105,12 +105,12 @@ def test_update_device_config_invalid_key(hass, test_client): @asyncio.coroutine -def test_update_device_config_invalid_data(hass, test_client): +def test_update_device_config_invalid_data(hass, aiohttp_client): """Test updating device config.""" with patch.object(config, 'SECTIONS', ['group']): yield from async_setup_component(hass, 'config', {}) - client = yield from test_client(hass.http.app) + client = yield from aiohttp_client(hass.http.app) resp = yield from client.post( '/api/config/group/config/hello_beer', data=json.dumps({ @@ -121,12 +121,12 @@ def test_update_device_config_invalid_data(hass, test_client): @asyncio.coroutine -def test_update_device_config_invalid_json(hass, test_client): +def test_update_device_config_invalid_json(hass, aiohttp_client): """Test updating device config.""" with patch.object(config, 'SECTIONS', ['group']): yield from async_setup_component(hass, 'config', {}) - client = yield from test_client(hass.http.app) + client = yield from aiohttp_client(hass.http.app) resp = yield from client.post( '/api/config/group/config/hello_beer', data='not json') diff --git a/tests/components/config/test_hassbian.py b/tests/components/config/test_hassbian.py index 9038ccc6aa4..85fbf0c2e5a 100644 --- a/tests/components/config/test_hassbian.py +++ b/tests/components/config/test_hassbian.py @@ -34,13 +34,13 @@ def test_setup_check_env_works(hass, loop): @asyncio.coroutine -def test_get_suites(hass, test_client): +def test_get_suites(hass, aiohttp_client): """Test getting suites.""" with patch.dict(os.environ, {'FORCE_HASSBIAN': '1'}), \ patch.object(config, 'SECTIONS', ['hassbian']): yield from async_setup_component(hass, 'config', {}) - client = yield from test_client(hass.http.app) + client = yield from aiohttp_client(hass.http.app) resp = yield from client.get('/api/config/hassbian/suites') assert resp.status == 200 result = yield from resp.json() @@ -53,13 +53,13 @@ def test_get_suites(hass, test_client): @asyncio.coroutine -def test_install_suite(hass, test_client): +def test_install_suite(hass, aiohttp_client): """Test getting suites.""" with patch.dict(os.environ, {'FORCE_HASSBIAN': '1'}), \ patch.object(config, 'SECTIONS', ['hassbian']): yield from async_setup_component(hass, 'config', {}) - client = yield from test_client(hass.http.app) + client = yield from aiohttp_client(hass.http.app) resp = yield from client.post( '/api/config/hassbian/suites/openzwave/install') assert resp.status == 200 diff --git a/tests/components/config/test_init.py b/tests/components/config/test_init.py index 2d5d814ac8a..57ea7e7a492 100644 --- a/tests/components/config/test_init.py +++ b/tests/components/config/test_init.py @@ -17,7 +17,7 @@ def test_config_setup(hass, loop): @asyncio.coroutine -def test_load_on_demand_already_loaded(hass, test_client): +def test_load_on_demand_already_loaded(hass, aiohttp_client): """Test getting suites.""" mock_component(hass, 'zwave') @@ -34,7 +34,7 @@ def test_load_on_demand_already_loaded(hass, test_client): @asyncio.coroutine -def test_load_on_demand_on_load(hass, test_client): +def test_load_on_demand_on_load(hass, aiohttp_client): """Test getting suites.""" with patch.object(config, 'SECTIONS', []), \ patch.object(config, 'ON_DEMAND', ['zwave']): diff --git a/tests/components/config/test_zwave.py b/tests/components/config/test_zwave.py index c98385a3c32..672bafeaf28 100644 --- a/tests/components/config/test_zwave.py +++ b/tests/components/config/test_zwave.py @@ -16,12 +16,12 @@ VIEW_NAME = 'api:config:zwave:device_config' @pytest.fixture -def client(loop, hass, test_client): +def client(loop, hass, aiohttp_client): """Client to communicate with Z-Wave config views.""" with patch.object(config, 'SECTIONS', ['zwave']): loop.run_until_complete(async_setup_component(hass, 'config', {})) - return loop.run_until_complete(test_client(hass.http.app)) + return loop.run_until_complete(aiohttp_client(hass.http.app)) @asyncio.coroutine diff --git a/tests/components/device_tracker/test_geofency.py b/tests/components/device_tracker/test_geofency.py index 5def6a217f4..a955dd0cc11 100644 --- a/tests/components/device_tracker/test_geofency.py +++ b/tests/components/device_tracker/test_geofency.py @@ -107,7 +107,7 @@ BEACON_EXIT_CAR = { @pytest.fixture -def geofency_client(loop, hass, test_client): +def geofency_client(loop, hass, aiohttp_client): """Geofency mock client.""" assert loop.run_until_complete(async_setup_component( hass, device_tracker.DOMAIN, { @@ -117,7 +117,7 @@ def geofency_client(loop, hass, test_client): }})) with patch('homeassistant.components.device_tracker.update_config'): - yield loop.run_until_complete(test_client(hass.http.app)) + yield loop.run_until_complete(aiohttp_client(hass.http.app)) @pytest.fixture(autouse=True) diff --git a/tests/components/device_tracker/test_locative.py b/tests/components/device_tracker/test_locative.py index 2476247e069..90adccf7703 100644 --- a/tests/components/device_tracker/test_locative.py +++ b/tests/components/device_tracker/test_locative.py @@ -19,7 +19,7 @@ def _url(data=None): @pytest.fixture -def locative_client(loop, hass, test_client): +def locative_client(loop, hass, aiohttp_client): """Locative mock client.""" assert loop.run_until_complete(async_setup_component( hass, device_tracker.DOMAIN, { @@ -29,7 +29,7 @@ def locative_client(loop, hass, test_client): })) with patch('homeassistant.components.device_tracker.update_config'): - yield loop.run_until_complete(test_client(hass.http.app)) + yield loop.run_until_complete(aiohttp_client(hass.http.app)) @asyncio.coroutine diff --git a/tests/components/device_tracker/test_meraki.py b/tests/components/device_tracker/test_meraki.py index 74fc577bca8..925ba6d66db 100644 --- a/tests/components/device_tracker/test_meraki.py +++ b/tests/components/device_tracker/test_meraki.py @@ -13,7 +13,7 @@ from homeassistant.components.device_tracker.meraki import URL @pytest.fixture -def meraki_client(loop, hass, test_client): +def meraki_client(loop, hass, aiohttp_client): """Meraki mock client.""" assert loop.run_until_complete(async_setup_component( hass, device_tracker.DOMAIN, { @@ -25,7 +25,7 @@ def meraki_client(loop, hass, test_client): } })) - yield loop.run_until_complete(test_client(hass.http.app)) + yield loop.run_until_complete(aiohttp_client(hass.http.app)) @asyncio.coroutine diff --git a/tests/components/device_tracker/test_owntracks_http.py b/tests/components/device_tracker/test_owntracks_http.py index be8bdd94ecc..d7b48cafe46 100644 --- a/tests/components/device_tracker/test_owntracks_http.py +++ b/tests/components/device_tracker/test_owntracks_http.py @@ -10,7 +10,7 @@ from tests.common import mock_coro, mock_component @pytest.fixture -def mock_client(hass, test_client): +def mock_client(hass, aiohttp_client): """Start the Hass HTTP component.""" mock_component(hass, 'group') mock_component(hass, 'zone') @@ -22,7 +22,7 @@ def mock_client(hass, test_client): 'platform': 'owntracks_http' } })) - return hass.loop.run_until_complete(test_client(hass.http.app)) + return hass.loop.run_until_complete(aiohttp_client(hass.http.app)) @pytest.fixture diff --git a/tests/components/emulated_hue/test_hue_api.py b/tests/components/emulated_hue/test_hue_api.py index 91988a76212..1617f327d27 100644 --- a/tests/components/emulated_hue/test_hue_api.py +++ b/tests/components/emulated_hue/test_hue_api.py @@ -118,7 +118,7 @@ def hass_hue(loop, hass): @pytest.fixture -def hue_client(loop, hass_hue, test_client): +def hue_client(loop, hass_hue, aiohttp_client): """Create web client for emulated hue api.""" web_app = hass_hue.http.app config = Config(None, { @@ -135,7 +135,7 @@ def hue_client(loop, hass_hue, test_client): HueOneLightStateView(config).register(web_app.router) HueOneLightChangeView(config).register(web_app.router) - return loop.run_until_complete(test_client(web_app)) + return loop.run_until_complete(aiohttp_client(web_app)) @asyncio.coroutine diff --git a/tests/components/google_assistant/test_google_assistant.py b/tests/components/google_assistant/test_google_assistant.py index cb319b67bb2..d45680d132e 100644 --- a/tests/components/google_assistant/test_google_assistant.py +++ b/tests/components/google_assistant/test_google_assistant.py @@ -27,7 +27,7 @@ AUTH_HEADER = {AUTHORIZATION: 'Bearer {}'.format(ACCESS_TOKEN)} @pytest.fixture -def assistant_client(loop, hass, test_client): +def assistant_client(loop, hass, aiohttp_client): """Create web client for the Google Assistant API.""" loop.run_until_complete( setup.async_setup_component(hass, 'google_assistant', { @@ -44,7 +44,7 @@ def assistant_client(loop, hass, test_client): } })) - return loop.run_until_complete(test_client(hass.http.app)) + return loop.run_until_complete(aiohttp_client(hass.http.app)) @pytest.fixture diff --git a/tests/components/hassio/conftest.py b/tests/components/hassio/conftest.py index 56d6cbe666e..9f20efc08a5 100644 --- a/tests/components/hassio/conftest.py +++ b/tests/components/hassio/conftest.py @@ -26,7 +26,7 @@ def hassio_env(): @pytest.fixture -def hassio_client(hassio_env, hass, test_client): +def hassio_client(hassio_env, hass, aiohttp_client): """Create mock hassio http client.""" with patch('homeassistant.components.hassio.HassIO.update_hass_api', Mock(return_value=mock_coro({"result": "ok"}))), \ @@ -38,7 +38,7 @@ def hassio_client(hassio_env, hass, test_client): 'api_password': API_PASSWORD } })) - yield hass.loop.run_until_complete(test_client(hass.http.app)) + yield hass.loop.run_until_complete(aiohttp_client(hass.http.app)) @pytest.fixture diff --git a/tests/components/http/test_auth.py b/tests/components/http/test_auth.py index 604ee9c0c9b..a44d17d513d 100644 --- a/tests/components/http/test_auth.py +++ b/tests/components/http/test_auth.py @@ -55,19 +55,19 @@ async def test_auth_middleware_loaded_by_default(hass): assert len(mock_setup.mock_calls) == 1 -async def test_access_without_password(app, test_client): +async def test_access_without_password(app, aiohttp_client): """Test access without password.""" setup_auth(app, [], None) - client = await test_client(app) + client = await aiohttp_client(app) resp = await client.get('/') assert resp.status == 200 -async def test_access_with_password_in_header(app, test_client): +async def test_access_with_password_in_header(app, aiohttp_client): """Test access with password in URL.""" setup_auth(app, [], API_PASSWORD) - client = await test_client(app) + client = await aiohttp_client(app) req = await client.get( '/', headers={HTTP_HEADER_HA_AUTH: API_PASSWORD}) @@ -78,10 +78,10 @@ async def test_access_with_password_in_header(app, test_client): assert req.status == 401 -async def test_access_with_password_in_query(app, test_client): +async def test_access_with_password_in_query(app, aiohttp_client): """Test access without password.""" setup_auth(app, [], API_PASSWORD) - client = await test_client(app) + client = await aiohttp_client(app) resp = await client.get('/', params={ 'api_password': API_PASSWORD @@ -97,10 +97,10 @@ async def test_access_with_password_in_query(app, test_client): assert resp.status == 401 -async def test_basic_auth_works(app, test_client): +async def test_basic_auth_works(app, aiohttp_client): """Test access with basic authentication.""" setup_auth(app, [], API_PASSWORD) - client = await test_client(app) + client = await aiohttp_client(app) req = await client.get( '/', @@ -125,7 +125,7 @@ async def test_basic_auth_works(app, test_client): assert req.status == 401 -async def test_access_with_trusted_ip(test_client): +async def test_access_with_trusted_ip(aiohttp_client): """Test access with an untrusted ip address.""" app = web.Application() app.router.add_get('/', mock_handler) @@ -133,7 +133,7 @@ async def test_access_with_trusted_ip(test_client): setup_auth(app, TRUSTED_NETWORKS, 'some-pass') set_mock_ip = mock_real_ip(app) - client = await test_client(app) + client = await aiohttp_client(app) for remote_addr in UNTRUSTED_ADDRESSES: set_mock_ip(remote_addr) diff --git a/tests/components/http/test_ban.py b/tests/components/http/test_ban.py index 2d7885d959f..c5691cf3e2a 100644 --- a/tests/components/http/test_ban.py +++ b/tests/components/http/test_ban.py @@ -15,7 +15,7 @@ from . import mock_real_ip BANNED_IPS = ['200.201.202.203', '100.64.0.2'] -async def test_access_from_banned_ip(hass, test_client): +async def test_access_from_banned_ip(hass, aiohttp_client): """Test accessing to server from banned IP. Both trusted and not.""" app = web.Application() setup_bans(hass, app, 5) @@ -24,7 +24,7 @@ async def test_access_from_banned_ip(hass, test_client): with patch('homeassistant.components.http.ban.load_ip_bans_config', return_value=[IpBan(banned_ip) for banned_ip in BANNED_IPS]): - client = await test_client(app) + client = await aiohttp_client(app) for remote_addr in BANNED_IPS: set_real_ip(remote_addr) @@ -54,7 +54,7 @@ async def test_ban_middleware_loaded_by_default(hass): assert len(mock_setup.mock_calls) == 1 -async def test_ip_bans_file_creation(hass, test_client): +async def test_ip_bans_file_creation(hass, aiohttp_client): """Testing if banned IP file created.""" app = web.Application() app['hass'] = hass @@ -70,7 +70,7 @@ async def test_ip_bans_file_creation(hass, test_client): with patch('homeassistant.components.http.ban.load_ip_bans_config', return_value=[IpBan(banned_ip) for banned_ip in BANNED_IPS]): - client = await test_client(app) + client = await aiohttp_client(app) m = mock_open() diff --git a/tests/components/http/test_cors.py b/tests/components/http/test_cors.py index 50464b36277..27367b4173e 100644 --- a/tests/components/http/test_cors.py +++ b/tests/components/http/test_cors.py @@ -47,12 +47,12 @@ async def mock_handler(request): @pytest.fixture -def client(loop, test_client): +def client(loop, aiohttp_client): """Fixture to setup a web.Application.""" app = web.Application() app.router.add_get('/', mock_handler) setup_cors(app, [TRUSTED_ORIGIN]) - return loop.run_until_complete(test_client(app)) + return loop.run_until_complete(aiohttp_client(app)) async def test_cors_requests(client): diff --git a/tests/components/http/test_data_validator.py b/tests/components/http/test_data_validator.py index 6cca1af8ccc..2b966daff6c 100644 --- a/tests/components/http/test_data_validator.py +++ b/tests/components/http/test_data_validator.py @@ -8,7 +8,7 @@ from homeassistant.components.http import HomeAssistantView from homeassistant.components.http.data_validator import RequestDataValidator -async def get_client(test_client, validator): +async def get_client(aiohttp_client, validator): """Generate a client that hits a view decorated with validator.""" app = web.Application() app['hass'] = Mock(is_running=True) @@ -24,14 +24,14 @@ async def get_client(test_client, validator): return b'' TestView().register(app.router) - client = await test_client(app) + client = await aiohttp_client(app) return client -async def test_validator(test_client): +async def test_validator(aiohttp_client): """Test the validator.""" client = await get_client( - test_client, RequestDataValidator(vol.Schema({ + aiohttp_client, RequestDataValidator(vol.Schema({ vol.Required('test'): str }))) @@ -49,10 +49,10 @@ async def test_validator(test_client): assert resp.status == 400 -async def test_validator_allow_empty(test_client): +async def test_validator_allow_empty(aiohttp_client): """Test the validator with empty data.""" client = await get_client( - test_client, RequestDataValidator(vol.Schema({ + aiohttp_client, RequestDataValidator(vol.Schema({ # Although we allow empty, our schema should still be able # to validate an empty dict. vol.Optional('test'): str diff --git a/tests/components/http/test_init.py b/tests/components/http/test_init.py index 1dcf45f48c3..c02e203444f 100644 --- a/tests/components/http/test_init.py +++ b/tests/components/http/test_init.py @@ -15,12 +15,13 @@ class TestView(http.HomeAssistantView): return 'hello' -async def test_registering_view_while_running(hass, test_client, unused_port): +async def test_registering_view_while_running(hass, aiohttp_client, + aiohttp_unused_port): """Test that we can register a view while the server is running.""" await async_setup_component( hass, http.DOMAIN, { http.DOMAIN: { - http.CONF_SERVER_PORT: unused_port(), + http.CONF_SERVER_PORT: aiohttp_unused_port(), } } ) @@ -73,17 +74,16 @@ async def test_api_no_base_url(hass): assert hass.config.api.base_url == 'http://127.0.0.1:8123' -async def test_not_log_password(hass, unused_port, test_client, caplog): +async def test_not_log_password(hass, aiohttp_client, caplog): """Test access with password doesn't get logged.""" result = await async_setup_component(hass, 'api', { 'http': { - http.CONF_SERVER_PORT: unused_port(), http.CONF_API_PASSWORD: 'some-pass' } }) assert result - client = await test_client(hass.http.app) + client = await aiohttp_client(hass.http.app) resp = await client.get('/api/', params={ 'api_password': 'some-pass' diff --git a/tests/components/http/test_real_ip.py b/tests/components/http/test_real_ip.py index 3e4f9023537..61846eb94c2 100644 --- a/tests/components/http/test_real_ip.py +++ b/tests/components/http/test_real_ip.py @@ -11,13 +11,13 @@ async def mock_handler(request): return web.Response(text=str(request[KEY_REAL_IP])) -async def test_ignore_x_forwarded_for(test_client): +async def test_ignore_x_forwarded_for(aiohttp_client): """Test that we get the IP from the transport.""" app = web.Application() app.router.add_get('/', mock_handler) setup_real_ip(app, False) - mock_api_client = await test_client(app) + mock_api_client = await aiohttp_client(app) resp = await mock_api_client.get('/', headers={ X_FORWARDED_FOR: '255.255.255.255' @@ -27,13 +27,13 @@ async def test_ignore_x_forwarded_for(test_client): assert text != '255.255.255.255' -async def test_use_x_forwarded_for(test_client): +async def test_use_x_forwarded_for(aiohttp_client): """Test that we get the IP from the transport.""" app = web.Application() app.router.add_get('/', mock_handler) setup_real_ip(app, True) - mock_api_client = await test_client(app) + mock_api_client = await aiohttp_client(app) resp = await mock_api_client.get('/', headers={ X_FORWARDED_FOR: '255.255.255.255' diff --git a/tests/components/mailbox/test_init.py b/tests/components/mailbox/test_init.py index c9267fa8e8e..3377fcefcf5 100644 --- a/tests/components/mailbox/test_init.py +++ b/tests/components/mailbox/test_init.py @@ -9,7 +9,7 @@ import homeassistant.components.mailbox as mailbox @pytest.fixture -def mock_http_client(hass, test_client): +def mock_http_client(hass, aiohttp_client): """Start the Hass HTTP component.""" config = { mailbox.DOMAIN: { @@ -18,7 +18,7 @@ def mock_http_client(hass, test_client): } hass.loop.run_until_complete( async_setup_component(hass, mailbox.DOMAIN, config)) - return hass.loop.run_until_complete(test_client(hass.http.app)) + return hass.loop.run_until_complete(aiohttp_client(hass.http.app)) @asyncio.coroutine diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index 1dd89a92f04..b25479bb75a 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -59,7 +59,7 @@ class TestMQTTComponent(unittest.TestCase): """Helper for recording calls.""" self.calls.append(args) - def test_client_stops_on_home_assistant_start(self): + def aiohttp_client_stops_on_home_assistant_start(self): """Test if client stops on HA stop.""" self.hass.bus.fire(EVENT_HOMEASSISTANT_STOP) self.hass.block_till_done() @@ -156,7 +156,7 @@ class TestMQTTCallbacks(unittest.TestCase): """Helper for recording calls.""" self.calls.append(args) - def test_client_starts_on_home_assistant_mqtt_setup(self): + def aiohttp_client_starts_on_home_assistant_mqtt_setup(self): """Test if client is connected after mqtt init on bootstrap.""" self.assertEqual(self.hass.data['mqtt']._mqttc.connect.call_count, 1) diff --git a/tests/components/notify/test_html5.py b/tests/components/notify/test_html5.py index 9ec71020ef1..318f3c7512c 100644 --- a/tests/components/notify/test_html5.py +++ b/tests/components/notify/test_html5.py @@ -49,7 +49,7 @@ REGISTER_URL = '/api/notify.html5' PUBLISH_URL = '/api/notify.html5/callback' -async def mock_client(hass, test_client, registrations=None): +async def mock_client(hass, aiohttp_client, registrations=None): """Create a test client for HTML5 views.""" if registrations is None: registrations = {} @@ -62,7 +62,7 @@ async def mock_client(hass, test_client, registrations=None): } }) - return await test_client(hass.http.app) + return await aiohttp_client(hass.http.app) class TestHtml5Notify(object): @@ -151,9 +151,9 @@ class TestHtml5Notify(object): assert mock_wp.mock_calls[4][2]['gcm_key'] is None -async def test_registering_new_device_view(hass, test_client): +async def test_registering_new_device_view(hass, aiohttp_client): """Test that the HTML view works.""" - client = await mock_client(hass, test_client) + client = await mock_client(hass, aiohttp_client) with patch('homeassistant.components.notify.html5.save_json') as mock_save: resp = await client.post(REGISTER_URL, data=json.dumps(SUBSCRIPTION_1)) @@ -165,9 +165,9 @@ async def test_registering_new_device_view(hass, test_client): } -async def test_registering_new_device_expiration_view(hass, test_client): +async def test_registering_new_device_expiration_view(hass, aiohttp_client): """Test that the HTML view works.""" - client = await mock_client(hass, test_client) + client = await mock_client(hass, aiohttp_client) with patch('homeassistant.components.notify.html5.save_json') as mock_save: resp = await client.post(REGISTER_URL, data=json.dumps(SUBSCRIPTION_4)) @@ -178,10 +178,10 @@ async def test_registering_new_device_expiration_view(hass, test_client): } -async def test_registering_new_device_fails_view(hass, test_client): +async def test_registering_new_device_fails_view(hass, aiohttp_client): """Test subs. are not altered when registering a new device fails.""" registrations = {} - client = await mock_client(hass, test_client, registrations) + client = await mock_client(hass, aiohttp_client, registrations) with patch('homeassistant.components.notify.html5.save_json', side_effect=HomeAssistantError()): @@ -191,10 +191,10 @@ async def test_registering_new_device_fails_view(hass, test_client): assert registrations == {} -async def test_registering_existing_device_view(hass, test_client): +async def test_registering_existing_device_view(hass, aiohttp_client): """Test subscription is updated when registering existing device.""" registrations = {} - client = await mock_client(hass, test_client, registrations) + client = await mock_client(hass, aiohttp_client, registrations) with patch('homeassistant.components.notify.html5.save_json') as mock_save: await client.post(REGISTER_URL, data=json.dumps(SUBSCRIPTION_1)) @@ -209,10 +209,10 @@ async def test_registering_existing_device_view(hass, test_client): } -async def test_registering_existing_device_fails_view(hass, test_client): +async def test_registering_existing_device_fails_view(hass, aiohttp_client): """Test sub. is not updated when registering existing device fails.""" registrations = {} - client = await mock_client(hass, test_client, registrations) + client = await mock_client(hass, aiohttp_client, registrations) with patch('homeassistant.components.notify.html5.save_json') as mock_save: await client.post(REGISTER_URL, data=json.dumps(SUBSCRIPTION_1)) @@ -225,9 +225,9 @@ async def test_registering_existing_device_fails_view(hass, test_client): } -async def test_registering_new_device_validation(hass, test_client): +async def test_registering_new_device_validation(hass, aiohttp_client): """Test various errors when registering a new device.""" - client = await mock_client(hass, test_client) + client = await mock_client(hass, aiohttp_client) resp = await client.post(REGISTER_URL, data=json.dumps({ 'browser': 'invalid browser', @@ -249,13 +249,13 @@ async def test_registering_new_device_validation(hass, test_client): assert resp.status == 400 -async def test_unregistering_device_view(hass, test_client): +async def test_unregistering_device_view(hass, aiohttp_client): """Test that the HTML unregister view works.""" registrations = { 'some device': SUBSCRIPTION_1, 'other device': SUBSCRIPTION_2, } - client = await mock_client(hass, test_client, registrations) + client = await mock_client(hass, aiohttp_client, registrations) with patch('homeassistant.components.notify.html5.save_json') as mock_save: resp = await client.delete(REGISTER_URL, data=json.dumps({ @@ -269,11 +269,11 @@ async def test_unregistering_device_view(hass, test_client): } -async def test_unregister_device_view_handle_unknown_subscription(hass, - test_client): +async def test_unregister_device_view_handle_unknown_subscription( + hass, aiohttp_client): """Test that the HTML unregister view handles unknown subscriptions.""" registrations = {} - client = await mock_client(hass, test_client, registrations) + client = await mock_client(hass, aiohttp_client, registrations) with patch('homeassistant.components.notify.html5.save_json') as mock_save: resp = await client.delete(REGISTER_URL, data=json.dumps({ @@ -285,13 +285,14 @@ async def test_unregister_device_view_handle_unknown_subscription(hass, assert len(mock_save.mock_calls) == 0 -async def test_unregistering_device_view_handles_save_error(hass, test_client): +async def test_unregistering_device_view_handles_save_error( + hass, aiohttp_client): """Test that the HTML unregister view handles save errors.""" registrations = { 'some device': SUBSCRIPTION_1, 'other device': SUBSCRIPTION_2, } - client = await mock_client(hass, test_client, registrations) + client = await mock_client(hass, aiohttp_client, registrations) with patch('homeassistant.components.notify.html5.save_json', side_effect=HomeAssistantError()): @@ -306,9 +307,9 @@ async def test_unregistering_device_view_handles_save_error(hass, test_client): } -async def test_callback_view_no_jwt(hass, test_client): +async def test_callback_view_no_jwt(hass, aiohttp_client): """Test that the notification callback view works without JWT.""" - client = await mock_client(hass, test_client) + client = await mock_client(hass, aiohttp_client) resp = await client.post(PUBLISH_URL, data=json.dumps({ 'type': 'push', 'tag': '3bc28d69-0921-41f1-ac6a-7a627ba0aa72' @@ -317,12 +318,12 @@ async def test_callback_view_no_jwt(hass, test_client): assert resp.status == 401, resp.response -async def test_callback_view_with_jwt(hass, test_client): +async def test_callback_view_with_jwt(hass, aiohttp_client): """Test that the notification callback view works with JWT.""" registrations = { 'device': SUBSCRIPTION_1 } - client = await mock_client(hass, test_client, registrations) + client = await mock_client(hass, aiohttp_client, registrations) with patch('pywebpush.WebPusher') as mock_wp: await hass.services.async_call('notify', 'notify', { diff --git a/tests/components/sensor/test_mhz19.py b/tests/components/sensor/test_mhz19.py index 6948a952c31..6d071489691 100644 --- a/tests/components/sensor/test_mhz19.py +++ b/tests/components/sensor/test_mhz19.py @@ -52,7 +52,7 @@ class TestMHZ19Sensor(unittest.TestCase): @patch('pmsensor.co2sensor.read_mh_z19_with_temperature', side_effect=OSError('test error')) - def test_client_update_oserror(self, mock_function): + def aiohttp_client_update_oserror(self, mock_function): """Test MHZClient when library throws OSError.""" from pmsensor import co2sensor client = mhz19.MHZClient(co2sensor, 'test.serial') @@ -61,7 +61,7 @@ class TestMHZ19Sensor(unittest.TestCase): @patch('pmsensor.co2sensor.read_mh_z19_with_temperature', return_value=(5001, 24)) - def test_client_update_ppm_overflow(self, mock_function): + def aiohttp_client_update_ppm_overflow(self, mock_function): """Test MHZClient when ppm is too high.""" from pmsensor import co2sensor client = mhz19.MHZClient(co2sensor, 'test.serial') @@ -70,7 +70,7 @@ class TestMHZ19Sensor(unittest.TestCase): @patch('pmsensor.co2sensor.read_mh_z19_with_temperature', return_value=(1000, 24)) - def test_client_update_good_read(self, mock_function): + def aiohttp_client_update_good_read(self, mock_function): """Test MHZClient when ppm is too high.""" from pmsensor import co2sensor client = mhz19.MHZClient(co2sensor, 'test.serial') diff --git a/tests/components/test_api.py b/tests/components/test_api.py index 69b9bfa69de..6d5bec046f1 100644 --- a/tests/components/test_api.py +++ b/tests/components/test_api.py @@ -11,10 +11,10 @@ from homeassistant.setup import async_setup_component @pytest.fixture -def mock_api_client(hass, test_client): +def mock_api_client(hass, aiohttp_client): """Start the Hass HTTP component.""" hass.loop.run_until_complete(async_setup_component(hass, 'api', {})) - return hass.loop.run_until_complete(test_client(hass.http.app)) + return hass.loop.run_until_complete(aiohttp_client(hass.http.app)) @asyncio.coroutine diff --git a/tests/components/test_conversation.py b/tests/components/test_conversation.py index 466dc57017a..bde00e10928 100644 --- a/tests/components/test_conversation.py +++ b/tests/components/test_conversation.py @@ -93,7 +93,7 @@ def test_register_before_setup(hass): @asyncio.coroutine -def test_http_processing_intent(hass, test_client): +def test_http_processing_intent(hass, aiohttp_client): """Test processing intent via HTTP API.""" class TestIntentHandler(intent.IntentHandler): intent_type = 'OrderBeer' @@ -122,7 +122,7 @@ def test_http_processing_intent(hass, test_client): }) assert result - client = yield from test_client(hass.http.app) + client = yield from aiohttp_client(hass.http.app) resp = yield from client.post('/api/conversation/process', json={ 'text': 'I would like the Grolsch beer' }) @@ -224,7 +224,7 @@ def test_toggle_intent(hass, sentence): @asyncio.coroutine -def test_http_api(hass, test_client): +def test_http_api(hass, aiohttp_client): """Test the HTTP conversation API.""" result = yield from component.async_setup(hass, {}) assert result @@ -232,7 +232,7 @@ def test_http_api(hass, test_client): result = yield from async_setup_component(hass, 'conversation', {}) assert result - client = yield from test_client(hass.http.app) + client = yield from aiohttp_client(hass.http.app) hass.states.async_set('light.kitchen', 'off') calls = async_mock_service(hass, 'homeassistant', 'turn_on') @@ -249,7 +249,7 @@ def test_http_api(hass, test_client): @asyncio.coroutine -def test_http_api_wrong_data(hass, test_client): +def test_http_api_wrong_data(hass, aiohttp_client): """Test the HTTP conversation API.""" result = yield from component.async_setup(hass, {}) assert result @@ -257,7 +257,7 @@ def test_http_api_wrong_data(hass, test_client): result = yield from async_setup_component(hass, 'conversation', {}) assert result - client = yield from test_client(hass.http.app) + client = yield from aiohttp_client(hass.http.app) resp = yield from client.post('/api/conversation/process', json={ 'text': 123 diff --git a/tests/components/test_frontend.py b/tests/components/test_frontend.py index c4ade7f5c19..c742e215738 100644 --- a/tests/components/test_frontend.py +++ b/tests/components/test_frontend.py @@ -12,14 +12,14 @@ from homeassistant.components.frontend import ( @pytest.fixture -def mock_http_client(hass, test_client): +def mock_http_client(hass, aiohttp_client): """Start the Hass HTTP component.""" hass.loop.run_until_complete(async_setup_component(hass, 'frontend', {})) - return hass.loop.run_until_complete(test_client(hass.http.app)) + return hass.loop.run_until_complete(aiohttp_client(hass.http.app)) @pytest.fixture -def mock_http_client_with_themes(hass, test_client): +def mock_http_client_with_themes(hass, aiohttp_client): """Start the Hass HTTP component.""" hass.loop.run_until_complete(async_setup_component(hass, 'frontend', { DOMAIN: { @@ -29,11 +29,11 @@ def mock_http_client_with_themes(hass, test_client): } } }})) - return hass.loop.run_until_complete(test_client(hass.http.app)) + return hass.loop.run_until_complete(aiohttp_client(hass.http.app)) @pytest.fixture -def mock_http_client_with_urls(hass, test_client): +def mock_http_client_with_urls(hass, aiohttp_client): """Start the Hass HTTP component.""" hass.loop.run_until_complete(async_setup_component(hass, 'frontend', { DOMAIN: { @@ -42,7 +42,7 @@ def mock_http_client_with_urls(hass, test_client): CONF_EXTRA_HTML_URL_ES5: ["https://domain.com/my_extra_url_es5.html"] }})) - return hass.loop.run_until_complete(test_client(hass.http.app)) + return hass.loop.run_until_complete(aiohttp_client(hass.http.app)) @asyncio.coroutine diff --git a/tests/components/test_prometheus.py b/tests/components/test_prometheus.py index 052292b015d..6cc0e4fcada 100644 --- a/tests/components/test_prometheus.py +++ b/tests/components/test_prometheus.py @@ -7,14 +7,14 @@ import homeassistant.components.prometheus as prometheus @pytest.fixture -def prometheus_client(loop, hass, test_client): - """Initialize a test_client with Prometheus component.""" +def prometheus_client(loop, hass, aiohttp_client): + """Initialize a aiohttp_client with Prometheus component.""" assert loop.run_until_complete(async_setup_component( hass, prometheus.DOMAIN, {}, )) - return loop.run_until_complete(test_client(hass.http.app)) + return loop.run_until_complete(aiohttp_client(hass.http.app)) @asyncio.coroutine diff --git a/tests/components/test_ring.py b/tests/components/test_ring.py index 819f447f2f5..3837ec13061 100644 --- a/tests/components/test_ring.py +++ b/tests/components/test_ring.py @@ -1,4 +1,5 @@ """The tests for the Ring component.""" +from copy import deepcopy import os import unittest import requests_mock @@ -51,7 +52,7 @@ class TestRing(unittest.TestCase): """Test the setup when no login is configured.""" mock.post('https://api.ring.com/clients_api/session', text=load_fixture('ring_session.json')) - conf = self.config.copy() + conf = deepcopy(VALID_CONFIG) del conf['ring']['username'] assert not setup.setup_component(self.hass, ring.DOMAIN, conf) @@ -60,6 +61,6 @@ class TestRing(unittest.TestCase): """Test the setup when no password is configured.""" mock.post('https://api.ring.com/clients_api/session', text=load_fixture('ring_session.json')) - conf = self.config.copy() + conf = deepcopy(VALID_CONFIG) del conf['ring']['password'] assert not setup.setup_component(self.hass, ring.DOMAIN, conf) diff --git a/tests/components/test_rss_feed_template.py b/tests/components/test_rss_feed_template.py index 8b16b5519e9..36f68e57c9f 100644 --- a/tests/components/test_rss_feed_template.py +++ b/tests/components/test_rss_feed_template.py @@ -8,7 +8,7 @@ from homeassistant.setup import async_setup_component @pytest.fixture -def mock_http_client(loop, hass, test_client): +def mock_http_client(loop, hass, aiohttp_client): """Setup test fixture.""" config = { 'rss_feed_template': { @@ -21,7 +21,7 @@ def mock_http_client(loop, hass, test_client): loop.run_until_complete(async_setup_component(hass, 'rss_feed_template', config)) - return loop.run_until_complete(test_client(hass.http.app)) + return loop.run_until_complete(aiohttp_client(hass.http.app)) @asyncio.coroutine diff --git a/tests/components/test_shopping_list.py b/tests/components/test_shopping_list.py index 4203f7587ae..3131ae092a3 100644 --- a/tests/components/test_shopping_list.py +++ b/tests/components/test_shopping_list.py @@ -54,7 +54,7 @@ def test_recent_items_intent(hass): @asyncio.coroutine -def test_api_get_all(hass, test_client): +def test_api_get_all(hass, aiohttp_client): """Test the API.""" yield from async_setup_component(hass, 'shopping_list', {}) @@ -65,7 +65,7 @@ def test_api_get_all(hass, test_client): hass, 'test', 'HassShoppingListAddItem', {'item': {'value': 'wine'}} ) - client = yield from test_client(hass.http.app) + client = yield from aiohttp_client(hass.http.app) resp = yield from client.get('/api/shopping_list') assert resp.status == 200 @@ -78,7 +78,7 @@ def test_api_get_all(hass, test_client): @asyncio.coroutine -def test_api_update(hass, test_client): +def test_api_update(hass, aiohttp_client): """Test the API.""" yield from async_setup_component(hass, 'shopping_list', {}) @@ -92,7 +92,7 @@ def test_api_update(hass, test_client): beer_id = hass.data['shopping_list'].items[0]['id'] wine_id = hass.data['shopping_list'].items[1]['id'] - client = yield from test_client(hass.http.app) + client = yield from aiohttp_client(hass.http.app) resp = yield from client.post( '/api/shopping_list/item/{}'.format(beer_id), json={ 'name': 'soda' @@ -133,7 +133,7 @@ def test_api_update(hass, test_client): @asyncio.coroutine -def test_api_update_fails(hass, test_client): +def test_api_update_fails(hass, aiohttp_client): """Test the API.""" yield from async_setup_component(hass, 'shopping_list', {}) @@ -141,7 +141,7 @@ def test_api_update_fails(hass, test_client): hass, 'test', 'HassShoppingListAddItem', {'item': {'value': 'beer'}} ) - client = yield from test_client(hass.http.app) + client = yield from aiohttp_client(hass.http.app) resp = yield from client.post( '/api/shopping_list/non_existing', json={ 'name': 'soda' @@ -159,7 +159,7 @@ def test_api_update_fails(hass, test_client): @asyncio.coroutine -def test_api_clear_completed(hass, test_client): +def test_api_clear_completed(hass, aiohttp_client): """Test the API.""" yield from async_setup_component(hass, 'shopping_list', {}) @@ -173,7 +173,7 @@ def test_api_clear_completed(hass, test_client): beer_id = hass.data['shopping_list'].items[0]['id'] wine_id = hass.data['shopping_list'].items[1]['id'] - client = yield from test_client(hass.http.app) + client = yield from aiohttp_client(hass.http.app) # Mark beer as completed resp = yield from client.post( @@ -196,11 +196,11 @@ def test_api_clear_completed(hass, test_client): @asyncio.coroutine -def test_api_create(hass, test_client): +def test_api_create(hass, aiohttp_client): """Test the API.""" yield from async_setup_component(hass, 'shopping_list', {}) - client = yield from test_client(hass.http.app) + client = yield from aiohttp_client(hass.http.app) resp = yield from client.post('/api/shopping_list/item', json={ 'name': 'soda' }) @@ -217,11 +217,11 @@ def test_api_create(hass, test_client): @asyncio.coroutine -def test_api_create_fail(hass, test_client): +def test_api_create_fail(hass, aiohttp_client): """Test the API.""" yield from async_setup_component(hass, 'shopping_list', {}) - client = yield from test_client(hass.http.app) + client = yield from aiohttp_client(hass.http.app) resp = yield from client.post('/api/shopping_list/item', json={ 'name': 1234 }) diff --git a/tests/components/test_system_log.py b/tests/components/test_system_log.py index d119c60dba2..c440ef9c30c 100644 --- a/tests/components/test_system_log.py +++ b/tests/components/test_system_log.py @@ -14,16 +14,16 @@ _LOGGER = logging.getLogger('test_logger') @pytest.fixture(autouse=True) @asyncio.coroutine -def setup_test_case(hass, test_client): +def setup_test_case(hass, aiohttp_client): """Setup system_log component before test case.""" config = {'system_log': {'max_entries': 2}} yield from async_setup_component(hass, system_log.DOMAIN, config) @asyncio.coroutine -def get_error_log(hass, test_client, expected_count): +def get_error_log(hass, aiohttp_client, expected_count): """Fetch all entries from system_log via the API.""" - client = yield from test_client(hass.http.app) + client = yield from aiohttp_client(hass.http.app) resp = yield from client.get('/api/error/all') assert resp.status == 200 @@ -53,41 +53,41 @@ def get_frame(name): @asyncio.coroutine -def test_normal_logs(hass, test_client): +def test_normal_logs(hass, aiohttp_client): """Test that debug and info are not logged.""" _LOGGER.debug('debug') _LOGGER.info('info') # Assert done by get_error_log - yield from get_error_log(hass, test_client, 0) + yield from get_error_log(hass, aiohttp_client, 0) @asyncio.coroutine -def test_exception(hass, test_client): +def test_exception(hass, aiohttp_client): """Test that exceptions are logged and retrieved correctly.""" _generate_and_log_exception('exception message', 'log message') - log = (yield from get_error_log(hass, test_client, 1))[0] + log = (yield from get_error_log(hass, aiohttp_client, 1))[0] assert_log(log, 'exception message', 'log message', 'ERROR') @asyncio.coroutine -def test_warning(hass, test_client): +def test_warning(hass, aiohttp_client): """Test that warning are logged and retrieved correctly.""" _LOGGER.warning('warning message') - log = (yield from get_error_log(hass, test_client, 1))[0] + log = (yield from get_error_log(hass, aiohttp_client, 1))[0] assert_log(log, '', 'warning message', 'WARNING') @asyncio.coroutine -def test_error(hass, test_client): +def test_error(hass, aiohttp_client): """Test that errors are logged and retrieved correctly.""" _LOGGER.error('error message') - log = (yield from get_error_log(hass, test_client, 1))[0] + log = (yield from get_error_log(hass, aiohttp_client, 1))[0] assert_log(log, '', 'error message', 'ERROR') @asyncio.coroutine -def test_error_posted_as_event(hass, test_client): +def test_error_posted_as_event(hass, aiohttp_client): """Test that error are posted as events.""" events = [] @@ -106,26 +106,26 @@ def test_error_posted_as_event(hass, test_client): @asyncio.coroutine -def test_critical(hass, test_client): +def test_critical(hass, aiohttp_client): """Test that critical are logged and retrieved correctly.""" _LOGGER.critical('critical message') - log = (yield from get_error_log(hass, test_client, 1))[0] + log = (yield from get_error_log(hass, aiohttp_client, 1))[0] assert_log(log, '', 'critical message', 'CRITICAL') @asyncio.coroutine -def test_remove_older_logs(hass, test_client): +def test_remove_older_logs(hass, aiohttp_client): """Test that older logs are rotated out.""" _LOGGER.error('error message 1') _LOGGER.error('error message 2') _LOGGER.error('error message 3') - log = yield from get_error_log(hass, test_client, 2) + log = yield from get_error_log(hass, aiohttp_client, 2) assert_log(log[0], '', 'error message 3', 'ERROR') assert_log(log[1], '', 'error message 2', 'ERROR') @asyncio.coroutine -def test_clear_logs(hass, test_client): +def test_clear_logs(hass, aiohttp_client): """Test that the log can be cleared via a service call.""" _LOGGER.error('error message') @@ -135,7 +135,7 @@ def test_clear_logs(hass, test_client): yield from hass.async_block_till_done() # Assert done by get_error_log - yield from get_error_log(hass, test_client, 0) + yield from get_error_log(hass, aiohttp_client, 0) @asyncio.coroutine @@ -182,12 +182,12 @@ def test_write_choose_level(hass): @asyncio.coroutine -def test_unknown_path(hass, test_client): +def test_unknown_path(hass, aiohttp_client): """Test error logged from unknown path.""" _LOGGER.findCaller = MagicMock( return_value=('unknown_path', 0, None, None)) _LOGGER.error('error message') - log = (yield from get_error_log(hass, test_client, 1))[0] + log = (yield from get_error_log(hass, aiohttp_client, 1))[0] assert log['source'] == 'unknown_path' @@ -207,30 +207,30 @@ def log_error_from_test_path(path): @asyncio.coroutine -def test_homeassistant_path(hass, test_client): +def test_homeassistant_path(hass, aiohttp_client): """Test error logged from homeassistant path.""" with patch('homeassistant.components.system_log.HOMEASSISTANT_PATH', new=['venv_path/homeassistant']): log_error_from_test_path( 'venv_path/homeassistant/component/component.py') - log = (yield from get_error_log(hass, test_client, 1))[0] + log = (yield from get_error_log(hass, aiohttp_client, 1))[0] assert log['source'] == 'component/component.py' @asyncio.coroutine -def test_config_path(hass, test_client): +def test_config_path(hass, aiohttp_client): """Test error logged from config path.""" with patch.object(hass.config, 'config_dir', new='config'): log_error_from_test_path('config/custom_component/test.py') - log = (yield from get_error_log(hass, test_client, 1))[0] + log = (yield from get_error_log(hass, aiohttp_client, 1))[0] assert log['source'] == 'custom_component/test.py' @asyncio.coroutine -def test_netdisco_path(hass, test_client): +def test_netdisco_path(hass, aiohttp_client): """Test error logged from netdisco path.""" with patch.dict('sys.modules', netdisco=MagicMock(__path__=['venv_path/netdisco'])): log_error_from_test_path('venv_path/netdisco/disco_component.py') - log = (yield from get_error_log(hass, test_client, 1))[0] + log = (yield from get_error_log(hass, aiohttp_client, 1))[0] assert log['source'] == 'disco_component.py' diff --git a/tests/components/test_websocket_api.py b/tests/components/test_websocket_api.py index d0c129e512e..4deccf65209 100644 --- a/tests/components/test_websocket_api.py +++ b/tests/components/test_websocket_api.py @@ -16,12 +16,12 @@ API_PASSWORD = 'test1234' @pytest.fixture -def websocket_client(loop, hass, test_client): +def websocket_client(loop, hass, aiohttp_client): """Websocket client fixture connected to websocket server.""" assert loop.run_until_complete( async_setup_component(hass, 'websocket_api')) - client = loop.run_until_complete(test_client(hass.http.app)) + client = loop.run_until_complete(aiohttp_client(hass.http.app)) ws = loop.run_until_complete(client.ws_connect(wapi.URL)) auth_ok = loop.run_until_complete(ws.receive_json()) assert auth_ok['type'] == wapi.TYPE_AUTH_OK @@ -33,7 +33,7 @@ def websocket_client(loop, hass, test_client): @pytest.fixture -def no_auth_websocket_client(hass, loop, test_client): +def no_auth_websocket_client(hass, loop, aiohttp_client): """Websocket connection that requires authentication.""" assert loop.run_until_complete( async_setup_component(hass, 'websocket_api', { @@ -42,7 +42,7 @@ def no_auth_websocket_client(hass, loop, test_client): } })) - client = loop.run_until_complete(test_client(hass.http.app)) + client = loop.run_until_complete(aiohttp_client(hass.http.app)) ws = loop.run_until_complete(client.ws_connect(wapi.URL)) auth_ok = loop.run_until_complete(ws.receive_json()) diff --git a/tests/helpers/test_aiohttp_client.py b/tests/helpers/test_aiohttp_client.py index abe30d80a49..28bb31c8482 100644 --- a/tests/helpers/test_aiohttp_client.py +++ b/tests/helpers/test_aiohttp_client.py @@ -14,7 +14,7 @@ from tests.common import get_test_home_assistant @pytest.fixture -def camera_client(hass, test_client): +def camera_client(hass, aiohttp_client): """Fixture to fetch camera streams.""" assert hass.loop.run_until_complete(async_setup_component(hass, 'camera', { 'camera': { @@ -23,7 +23,7 @@ def camera_client(hass, test_client): 'mjpeg_url': 'http://example.com/mjpeg_stream', }})) - yield hass.loop.run_until_complete(test_client(hass.http.app)) + yield hass.loop.run_until_complete(aiohttp_client(hass.http.app)) class TestHelpersAiohttpClient(unittest.TestCase): diff --git a/tests/test_util/aiohttp.py b/tests/test_util/aiohttp.py index d661ffba477..e67d5de50d1 100644 --- a/tests/test_util/aiohttp.py +++ b/tests/test_util/aiohttp.py @@ -82,7 +82,8 @@ class AiohttpClientMocker: def create_session(self, loop): """Create a ClientSession that is bound to this mocker.""" session = ClientSession(loop=loop) - session._request = self.match_request + # Setting directly on `session` will raise deprecation warning + object.__setattr__(session, '_request', self.match_request) return session async def match_request(self, method, url, *, data=None, auth=None, From 456ff4e84b6a6b2160921824e62911f5e09573a4 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 15 Mar 2018 13:53:59 -0700 Subject: [PATCH 105/220] Tado: don't reference unset hass var (#13237) Tado: don't reference unset hass var --- homeassistant/components/device_tracker/tado.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/device_tracker/tado.py b/homeassistant/components/device_tracker/tado.py index 11d12322ff5..ef816338ce9 100644 --- a/homeassistant/components/device_tracker/tado.py +++ b/homeassistant/components/device_tracker/tado.py @@ -100,7 +100,7 @@ class TadoDeviceScanner(DeviceScanner): last_results = [] try: - with async_timeout.timeout(10, loop=self.hass.loop): + with async_timeout.timeout(10): # Format the URL here, so we can log the template URL if # anything goes wrong without exposing username and password. url = self.tadoapiurl.format( From d13bcf8412a74de965ab3bede8c85483c12207f5 Mon Sep 17 00:00:00 2001 From: Gerard Date: Thu, 15 Mar 2018 22:56:35 +0100 Subject: [PATCH 106/220] Add extra sensors for BMW ConnectedDrive (#12591) * Added extra sensors for BMW ConnectedDrive * Updates based on review of @MartinHjelmare * Updates based on 2nd review of @MartinHjelmare * Changed control flow for updates to support updates triggered by remote services. * updated library version number * Changed order of commands so that the UI looks consistent. State of lock is now set optimisitcally before getting proper update from the server. So that the state does not toggle in the UI. * Added comment on optimistic state * Updated requirements_all.txt * Revert access permission changes * Fix for Travis * Changes based on review by @MartinHjelmare --- .../binary_sensor/bmw_connected_drive.py | 117 ++++++++++++++++++ .../components/bmw_connected_drive.py | 2 +- .../components/lock/bmw_connected_drive.py | 108 ++++++++++++++++ .../components/sensor/bmw_connected_drive.py | 49 +++++--- 4 files changed, 259 insertions(+), 17 deletions(-) create mode 100644 homeassistant/components/binary_sensor/bmw_connected_drive.py create mode 100644 homeassistant/components/lock/bmw_connected_drive.py diff --git a/homeassistant/components/binary_sensor/bmw_connected_drive.py b/homeassistant/components/binary_sensor/bmw_connected_drive.py new file mode 100644 index 00000000000..0c848a57fbf --- /dev/null +++ b/homeassistant/components/binary_sensor/bmw_connected_drive.py @@ -0,0 +1,117 @@ +""" +Reads vehicle status from BMW connected drive portal. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/binary_sensor.bmw_connected_drive/ +""" +import asyncio +import logging + +from homeassistant.components.bmw_connected_drive import DOMAIN as BMW_DOMAIN +from homeassistant.components.binary_sensor import BinarySensorDevice + +DEPENDENCIES = ['bmw_connected_drive'] + +_LOGGER = logging.getLogger(__name__) + +SENSOR_TYPES = { + 'all_lids_closed': ['Doors', 'opening'], + 'all_windows_closed': ['Windows', 'opening'], + 'door_lock_state': ['Door lock state', 'safety'] +} + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the BMW sensors.""" + accounts = hass.data[BMW_DOMAIN] + _LOGGER.debug('Found BMW accounts: %s', + ', '.join([a.name for a in accounts])) + devices = [] + for account in accounts: + for vehicle in account.account.vehicles: + for key, value in sorted(SENSOR_TYPES.items()): + device = BMWConnectedDriveSensor(account, vehicle, key, + value[0], value[1]) + devices.append(device) + add_devices(devices, True) + + +class BMWConnectedDriveSensor(BinarySensorDevice): + """Representation of a BMW vehicle binary sensor.""" + + def __init__(self, account, vehicle, attribute: str, sensor_name, + device_class): + """Constructor.""" + self._account = account + self._vehicle = vehicle + self._attribute = attribute + self._name = sensor_name + self._device_class = device_class + self._state = None + + @property + def should_poll(self) -> bool: + """Data update is triggered from BMWConnectedDriveEntity.""" + return False + + @property + def name(self): + """Return the name of the binary sensor.""" + return self._name + + @property + def device_class(self): + """Return the class of the binary sensor.""" + return self._device_class + + @property + def is_on(self): + """Return the state of the binary sensor.""" + return self._state + + @property + def device_state_attributes(self): + """Return the state attributes of the binary sensor.""" + vehicle_state = self._vehicle.state + result = { + 'car': self._vehicle.modelName + } + + if self._attribute == 'all_lids_closed': + for lid in vehicle_state.lids: + result[lid.name] = lid.state.value + elif self._attribute == 'all_windows_closed': + for window in vehicle_state.windows: + result[window.name] = window.state.value + elif self._attribute == 'door_lock_state': + result['door_lock_state'] = vehicle_state.door_lock_state.value + + return result + + def update(self): + """Read new state data from the library.""" + vehicle_state = self._vehicle.state + + # device class opening: On means open, Off means closed + if self._attribute == 'all_lids_closed': + _LOGGER.debug("Status of lid: %s", vehicle_state.all_lids_closed) + self._state = not vehicle_state.all_lids_closed + if self._attribute == 'all_windows_closed': + self._state = not vehicle_state.all_windows_closed + # device class safety: On means unsafe, Off means safe + if self._attribute == 'door_lock_state': + # Possible values: LOCKED, SECURED, SELECTIVELOCKED, UNLOCKED + self._state = bool(vehicle_state.door_lock_state.value + in ('SELECTIVELOCKED', 'UNLOCKED')) + + def update_callback(self): + """Schedule a state update.""" + self.schedule_update_ha_state(True) + + @asyncio.coroutine + def async_added_to_hass(self): + """Add callback after being added to hass. + + Show latest data after startup. + """ + self._account.add_update_listener(self.update_callback) diff --git a/homeassistant/components/bmw_connected_drive.py b/homeassistant/components/bmw_connected_drive.py index 86048a56e22..9e9e2bafac5 100644 --- a/homeassistant/components/bmw_connected_drive.py +++ b/homeassistant/components/bmw_connected_drive.py @@ -37,7 +37,7 @@ CONFIG_SCHEMA = vol.Schema({ }, extra=vol.ALLOW_EXTRA) -BMW_COMPONENTS = ['device_tracker', 'sensor'] +BMW_COMPONENTS = ['binary_sensor', 'device_tracker', 'lock', 'sensor'] UPDATE_INTERVAL = 5 # in minutes diff --git a/homeassistant/components/lock/bmw_connected_drive.py b/homeassistant/components/lock/bmw_connected_drive.py new file mode 100644 index 00000000000..4592fd7cae9 --- /dev/null +++ b/homeassistant/components/lock/bmw_connected_drive.py @@ -0,0 +1,108 @@ +""" +Support for BMW cars with BMW ConnectedDrive. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/lock.bmw_connected_drive/ +""" +import asyncio +import logging + +from homeassistant.components.bmw_connected_drive import DOMAIN as BMW_DOMAIN +from homeassistant.components.lock import LockDevice +from homeassistant.const import STATE_LOCKED, STATE_UNLOCKED + +DEPENDENCIES = ['bmw_connected_drive'] + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the BMW Connected Drive lock.""" + accounts = hass.data[BMW_DOMAIN] + _LOGGER.debug('Found BMW accounts: %s', + ', '.join([a.name for a in accounts])) + devices = [] + for account in accounts: + for vehicle in account.account.vehicles: + device = BMWLock(account, vehicle, 'lock', 'BMW lock') + devices.append(device) + add_devices(devices, True) + + +class BMWLock(LockDevice): + """Representation of a BMW vehicle lock.""" + + def __init__(self, account, vehicle, attribute: str, sensor_name): + """Initialize the lock.""" + self._account = account + self._vehicle = vehicle + self._attribute = attribute + self._name = sensor_name + self._state = None + + @property + def should_poll(self): + """Do not poll this class. + + Updates are triggered from BMWConnectedDriveAccount. + """ + return False + + @property + def name(self): + """Return the name of the lock.""" + return self._name + + @property + def device_state_attributes(self): + """Return the state attributes of the lock.""" + vehicle_state = self._vehicle.state + return { + 'car': self._vehicle.modelName, + 'door_lock_state': vehicle_state.door_lock_state.value + } + + @property + def is_locked(self): + """Return true if lock is locked.""" + return self._state == STATE_LOCKED + + def lock(self, **kwargs): + """Lock the car.""" + _LOGGER.debug("%s: locking doors", self._vehicle.modelName) + # Optimistic state set here because it takes some time before the + # update callback response + self._state = STATE_LOCKED + self.schedule_update_ha_state() + self._vehicle.remote_services.trigger_remote_door_lock() + + def unlock(self, **kwargs): + """Unlock the car.""" + _LOGGER.debug("%s: unlocking doors", self._vehicle.modelName) + # Optimistic state set here because it takes some time before the + # update callback response + self._state = STATE_UNLOCKED + self.schedule_update_ha_state() + self._vehicle.remote_services.trigger_remote_door_unlock() + + def update(self): + """Update state of the lock.""" + _LOGGER.debug("%s: updating data for %s", self._vehicle.modelName, + self._attribute) + vehicle_state = self._vehicle.state + + # Possible values: LOCKED, SECURED, SELECTIVELOCKED, UNLOCKED + self._state = (STATE_LOCKED if vehicle_state.door_lock_state.value + in ('LOCKED', 'SECURED') else STATE_UNLOCKED) + + def update_callback(self): + """Schedule a state update.""" + self.schedule_update_ha_state(True) + + @asyncio.coroutine + def async_added_to_hass(self): + """Add callback after being added to hass. + + Show latest data after startup. + """ + self._account.add_update_listener(self.update_callback) diff --git a/homeassistant/components/sensor/bmw_connected_drive.py b/homeassistant/components/sensor/bmw_connected_drive.py index 26bfd19e6fc..76719763931 100644 --- a/homeassistant/components/sensor/bmw_connected_drive.py +++ b/homeassistant/components/sensor/bmw_connected_drive.py @@ -14,14 +14,16 @@ DEPENDENCIES = ['bmw_connected_drive'] _LOGGER = logging.getLogger(__name__) -LENGTH_ATTRIBUTES = [ - 'remaining_range_fuel', - 'mileage', - ] +LENGTH_ATTRIBUTES = { + 'remaining_range_fuel': ['Range (fuel)', 'mdi:ruler'], + 'mileage': ['Mileage', 'mdi:speedometer'] +} -VALID_ATTRIBUTES = LENGTH_ATTRIBUTES + [ - 'remaining_fuel', -] +VALID_ATTRIBUTES = { + 'remaining_fuel': ['Remaining Fuel', 'mdi:gas-station'] +} + +VALID_ATTRIBUTES.update(LENGTH_ATTRIBUTES) def setup_platform(hass, config, add_devices, discovery_info=None): @@ -32,23 +34,25 @@ def setup_platform(hass, config, add_devices, discovery_info=None): devices = [] for account in accounts: for vehicle in account.account.vehicles: - for sensor in VALID_ATTRIBUTES: - device = BMWConnectedDriveSensor(account, vehicle, sensor) + for key, value in sorted(VALID_ATTRIBUTES.items()): + device = BMWConnectedDriveSensor(account, vehicle, key, + value[0], value[1]) devices.append(device) - add_devices(devices) + add_devices(devices, True) class BMWConnectedDriveSensor(Entity): """Representation of a BMW vehicle sensor.""" - def __init__(self, account, vehicle, attribute: str): + def __init__(self, account, vehicle, attribute: str, sensor_name, icon): """Constructor.""" self._vehicle = vehicle self._account = account self._attribute = attribute self._state = None self._unit_of_measurement = None - self._name = '{} {}'.format(self._vehicle.modelName, self._attribute) + self._name = sensor_name + self._icon = icon @property def should_poll(self) -> bool: @@ -60,6 +64,11 @@ class BMWConnectedDriveSensor(Entity): """Return the name of the sensor.""" return self._name + @property + def icon(self): + """Icon to use in the frontend, if any.""" + return self._icon + @property def state(self): """Return the state of the sensor. @@ -74,9 +83,16 @@ class BMWConnectedDriveSensor(Entity): """Get the unit of measurement.""" return self._unit_of_measurement + @property + def device_state_attributes(self): + """Return the state attributes of the binary sensor.""" + return { + 'car': self._vehicle.modelName + } + def update(self) -> None: """Read new state data from the library.""" - _LOGGER.debug('Updating %s', self.entity_id) + _LOGGER.debug('Updating %s', self._vehicle.modelName) vehicle_state = self._vehicle.state self._state = getattr(vehicle_state, self._attribute) @@ -87,7 +103,9 @@ class BMWConnectedDriveSensor(Entity): else: self._unit_of_measurement = None - self.schedule_update_ha_state() + def update_callback(self): + """Schedule a state update.""" + self.schedule_update_ha_state(True) @asyncio.coroutine def async_added_to_hass(self): @@ -95,5 +113,4 @@ class BMWConnectedDriveSensor(Entity): Show latest data after startup. """ - self._account.add_update_listener(self.update) - yield from self.hass.async_add_job(self.update) + self._account.add_update_listener(self.update_callback) From de1ff1e9522ec04905e4784ac71281b489e21c9c Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Fri, 16 Mar 2018 00:12:43 +0100 Subject: [PATCH 107/220] Fix Sonos join/unjoin in scripts (#13248) --- homeassistant/components/media_player/sonos.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/media_player/sonos.py b/homeassistant/components/media_player/sonos.py index e124fbd0443..34f30b5c2f4 100644 --- a/homeassistant/components/media_player/sonos.py +++ b/homeassistant/components/media_player/sonos.py @@ -188,13 +188,18 @@ def setup_platform(hass, config, add_devices, discovery_info=None): master = [device for device in hass.data[DATA_SONOS].devices if device.entity_id == service.data[ATTR_MASTER]] if master: - master[0].join(devices) + with hass.data[DATA_SONOS].topology_lock: + master[0].join(devices) + return + + if service.service == SERVICE_UNJOIN: + with hass.data[DATA_SONOS].topology_lock: + for device in devices: + device.unjoin() return for device in devices: - if service.service == SERVICE_UNJOIN: - device.unjoin() - elif service.service == SERVICE_SNAPSHOT: + if service.service == SERVICE_SNAPSHOT: device.snapshot(service.data[ATTR_WITH_GROUP]) elif service.service == SERVICE_RESTORE: device.restore(service.data[ATTR_WITH_GROUP]) @@ -887,16 +892,19 @@ class SonosDevice(MediaPlayerDevice): def join(self, slaves): """Form a group with other players.""" if self._coordinator: - self.soco.unjoin() + self.unjoin() for slave in slaves: if slave.unique_id != self.unique_id: slave.soco.join(self.soco) + # pylint: disable=protected-access + slave._coordinator = self @soco_error() def unjoin(self): """Unjoin the player from a group.""" self.soco.unjoin() + self._coordinator = None @soco_error() def snapshot(self, with_group=True): From 2350ce96a606205cb9a57513fbf90019a476cf46 Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Fri, 16 Mar 2018 01:05:28 +0100 Subject: [PATCH 108/220] Homekit: New supported devices (#13244) * Fixed log message * Added support for scripts * Added support for lights * Small refactoring * Added support for humidity sensor * Added tests --- homeassistant/components/homekit/__init__.py | 15 +- .../components/homekit/accessories.py | 11 + homeassistant/components/homekit/const.py | 14 +- .../components/homekit/type_covers.py | 14 +- .../components/homekit/type_lights.py | 209 ++++++++++++++++++ .../homekit/type_security_systems.py | 12 +- .../components/homekit/type_sensors.py | 59 +++-- .../components/homekit/type_switches.py | 9 - .../components/homekit/type_thermostats.py | 12 +- .../homekit/test_get_accessories.py | 13 ++ tests/components/homekit/test_type_lights.py | 160 ++++++++++++++ tests/components/homekit/test_type_sensors.py | 31 ++- 12 files changed, 495 insertions(+), 64 deletions(-) create mode 100644 homeassistant/components/homekit/type_lights.py create mode 100644 tests/components/homekit/test_type_lights.py diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 63013bd8fc9..b74171b08f7 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -73,7 +73,8 @@ async def async_setup(hass, config): def get_accessory(hass, state, aid, config): """Take state and return an accessory object if supported.""" - _LOGGER.debug('%s: ') + _LOGGER.debug('', + state.entity_id, aid, config) if not aid: _LOGGER.warning('The entitiy "%s" is not supported, since it ' 'generates an invalid aid, please change it.', @@ -87,6 +88,11 @@ def get_accessory(hass, state, aid, config): state.entity_id, 'TemperatureSensor') return TYPES['TemperatureSensor'](hass, state.entity_id, state.name, aid=aid) + elif unit == '%': + _LOGGER.debug('Add "%s" as %s"', + state.entity_id, 'HumiditySensor') + return TYPES['HumiditySensor'](hass, state.entity_id, state.name, + aid=aid) elif state.domain == 'cover': # Only add covers that support set_cover_position @@ -114,8 +120,11 @@ def get_accessory(hass, state, aid, config): return TYPES['Thermostat'](hass, state.entity_id, state.name, support_auto, aid=aid) + elif state.domain == 'light': + return TYPES['Light'](hass, state.entity_id, state.name, aid=aid) + elif state.domain == 'switch' or state.domain == 'remote' \ - or state.domain == 'input_boolean': + or state.domain == 'input_boolean' or state.domain == 'script': _LOGGER.debug('Add "%s" as "%s"', state.entity_id, 'Switch') return TYPES['Switch'](hass, state.entity_id, state.name, aid=aid) @@ -175,7 +184,7 @@ class HomeKit(): # pylint: disable=unused-variable from . import ( # noqa F401 - type_covers, type_security_systems, type_sensors, + type_covers, type_lights, type_security_systems, type_sensors, type_switches, type_thermostats) for state in self._hass.states.all(): diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index 0af25bc4453..4c4409e6dfc 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -4,6 +4,8 @@ import logging from pyhap.accessory import Accessory, Bridge, Category from pyhap.accessory_driver import AccessoryDriver +from homeassistant.helpers.event import async_track_state_change + from .const import ( ACCESSORY_MODEL, ACCESSORY_NAME, BRIDGE_MODEL, BRIDGE_NAME, MANUFACTURER, SERV_ACCESSORY_INFO, SERV_BRIDGING_STATE, @@ -49,6 +51,8 @@ def override_properties(char, properties=None, valid_values=None): class HomeAccessory(Accessory): """Adapter class for Accessory.""" + # pylint: disable=no-member + def __init__(self, name=ACCESSORY_NAME, model=ACCESSORY_MODEL, category='OTHER', **kwargs): """Initialize a Accessory object.""" @@ -59,6 +63,13 @@ class HomeAccessory(Accessory): def _set_services(self): add_preload_service(self, SERV_ACCESSORY_INFO) + def run(self): + """Method called by accessory after driver is started.""" + state = self._hass.states.get(self._entity_id) + self.update_state(new_state=state) + async_track_state_change( + self._hass, self._entity_id, self.update_state) + class HomeBridge(Bridge): """Adapter class for Bridge.""" diff --git a/homeassistant/components/homekit/const.py b/homeassistant/components/homekit/const.py index d2b1caffe53..a45c8298b78 100644 --- a/homeassistant/components/homekit/const.py +++ b/homeassistant/components/homekit/const.py @@ -23,10 +23,18 @@ BRIDGE_MODEL = 'homekit.bridge' BRIDGE_NAME = 'Home Assistant' MANUFACTURER = 'HomeAssistant' +# #### Categories #### +CATEGORY_LIGHT = 'LIGHTBULB' +CATEGORY_SENSOR = 'SENSOR' + # #### Services #### SERV_ACCESSORY_INFO = 'AccessoryInformation' SERV_BRIDGING_STATE = 'BridgingState' +SERV_HUMIDITY_SENSOR = 'HumiditySensor' +# CurrentRelativeHumidity | StatusActive, StatusFault, StatusTampered, +# StatusLowBattery, Name +SERV_LIGHTBULB = 'Lightbulb' # On | Brightness, Hue, Saturation, Name SERV_SECURITY_SYSTEM = 'SecuritySystem' SERV_SWITCH = 'Switch' SERV_TEMPERATURE_SENSOR = 'TemperatureSensor' @@ -36,20 +44,24 @@ SERV_WINDOW_COVERING = 'WindowCovering' # #### Characteristics #### CHAR_ACC_IDENTIFIER = 'AccessoryIdentifier' +CHAR_BRIGHTNESS = 'Brightness' # Int | [0, 100] CHAR_CATEGORY = 'Category' CHAR_COOLING_THRESHOLD_TEMPERATURE = 'CoolingThresholdTemperature' CHAR_CURRENT_HEATING_COOLING = 'CurrentHeatingCoolingState' CHAR_CURRENT_POSITION = 'CurrentPosition' +CHAR_CURRENT_HUMIDITY = 'CurrentRelativeHumidity' # percent CHAR_CURRENT_SECURITY_STATE = 'SecuritySystemCurrentState' CHAR_CURRENT_TEMPERATURE = 'CurrentTemperature' CHAR_HEATING_THRESHOLD_TEMPERATURE = 'HeatingThresholdTemperature' +CHAR_HUE = 'Hue' # arcdegress | [0, 360] CHAR_LINK_QUALITY = 'LinkQuality' CHAR_MANUFACTURER = 'Manufacturer' CHAR_MODEL = 'Model' CHAR_NAME = 'Name' -CHAR_ON = 'On' +CHAR_ON = 'On' # boolean CHAR_POSITION_STATE = 'PositionState' CHAR_REACHABLE = 'Reachable' +CHAR_SATURATION = 'Saturation' # percent CHAR_SERIAL_NUMBER = 'SerialNumber' CHAR_TARGET_HEATING_COOLING = 'TargetHeatingCoolingState' CHAR_TARGET_POSITION = 'TargetPosition' diff --git a/homeassistant/components/homekit/type_covers.py b/homeassistant/components/homekit/type_covers.py index 0110bff3185..36cfa4d635a 100644 --- a/homeassistant/components/homekit/type_covers.py +++ b/homeassistant/components/homekit/type_covers.py @@ -2,7 +2,6 @@ import logging from homeassistant.components.cover import ATTR_CURRENT_POSITION -from homeassistant.helpers.event import async_track_state_change from . import TYPES from .accessories import HomeAccessory, add_preload_service @@ -22,7 +21,7 @@ class WindowCovering(HomeAccessory): """ def __init__(self, hass, entity_id, display_name, *args, **kwargs): - """Initialize a Window accessory object.""" + """Initialize a WindowCovering accessory object.""" super().__init__(display_name, entity_id, 'WINDOW_COVERING', *args, **kwargs) @@ -45,14 +44,6 @@ class WindowCovering(HomeAccessory): self.char_target_position.setter_callback = self.move_cover - def run(self): - """Method called be object after driver is started.""" - state = self._hass.states.get(self._entity_id) - self.update_cover_position(new_state=state) - - async_track_state_change( - self._hass, self._entity_id, self.update_cover_position) - def move_cover(self, value): """Move cover to value if call came from HomeKit.""" if value != self.current_position: @@ -65,8 +56,7 @@ class WindowCovering(HomeAccessory): self._hass.components.cover.set_cover_position( value, self._entity_id) - def update_cover_position(self, entity_id=None, old_state=None, - new_state=None): + def update_state(self, entity_id=None, old_state=None, new_state=None): """Update cover position after state changed.""" if new_state is None: return diff --git a/homeassistant/components/homekit/type_lights.py b/homeassistant/components/homekit/type_lights.py new file mode 100644 index 00000000000..107ad1db1e4 --- /dev/null +++ b/homeassistant/components/homekit/type_lights.py @@ -0,0 +1,209 @@ +"""Class to hold all light accessories.""" +import logging + +from homeassistant.components.light import ( + ATTR_RGB_COLOR, ATTR_BRIGHTNESS, + SUPPORT_BRIGHTNESS, SUPPORT_RGB_COLOR) +from homeassistant.const import ATTR_SUPPORTED_FEATURES, STATE_ON, STATE_OFF + +from . import TYPES +from .accessories import HomeAccessory, add_preload_service +from .const import ( + CATEGORY_LIGHT, SERV_LIGHTBULB, + CHAR_BRIGHTNESS, CHAR_HUE, CHAR_ON, CHAR_SATURATION) + +_LOGGER = logging.getLogger(__name__) + +RGB_COLOR = 'rgb_color' + + +class Color: + """Class to handle color conversions.""" + + # pylint: disable=invalid-name + + def __init__(self, hue=None, saturation=None): + """Initialize a new Color object.""" + self.hue = hue # [0, 360] + self.saturation = saturation # [0, 1] + + def calc_hsv_to_rgb(self): + """Convert hsv_color value to rgb_color.""" + if not self.hue or not self.saturation: + return [None] * 3 + + i = int(self.hue / 60) + f = self.hue / 60 - i + v = 1 + p = 1 - self.saturation + q = 1 - self.saturation * f + t = 1 - self.saturation * (1 - f) + + rgb = [] + if i in [0, 6]: + rgb = [v, t, p] + elif i == 1: + rgb = [q, v, p] + elif i == 2: + rgb = [p, v, t] + elif i == 3: + rgb = [p, q, v] + elif i == 4: + rgb = [t, p, v] + elif i == 5: + rgb = [v, p, q] + + return [round(c * 255) for c in rgb] + + @classmethod + def calc_rgb_to_hsv(cls, rgb_color): + """Convert a give rgb_color back to a hsv_color.""" + rgb_color = [c / 255 for c in rgb_color] + c_max = max(rgb_color) + c_min = min(rgb_color) + c_diff = c_max - c_min + r, g, b = rgb_color + + hue, saturation = 0, 0 + if c_max == r: + hue = 60 * (0 + (g - b) / c_diff) + elif c_max == g: + hue = 60 * (2 + (b - r) / c_diff) + elif c_max == b: + hue = 60 * (4 + (r - g) / c_diff) + + hue = round(hue + 360) if hue < 0 else round(hue) + + if c_max != 0: + saturation = round((c_max - c_min) / c_max * 100) + + return (hue, saturation) + + +@TYPES.register('Light') +class Light(HomeAccessory): + """Generate a Light accessory for a light entity. + + Currently supports: state, brightness, rgb_color. + """ + + def __init__(self, hass, entity_id, name, *args, **kwargs): + """Initialize a new Light accessory object.""" + super().__init__(name, entity_id, CATEGORY_LIGHT, *args, **kwargs) + + self._hass = hass + self._entity_id = entity_id + self._flag = {CHAR_ON: False, CHAR_BRIGHTNESS: False, + CHAR_HUE: False, CHAR_SATURATION: False, + RGB_COLOR: False} + + self.color = Color() + + self.chars = [] + self._features = self._hass.states.get(self._entity_id) \ + .attributes.get(ATTR_SUPPORTED_FEATURES) + if self._features & SUPPORT_BRIGHTNESS: + self.chars.append(CHAR_BRIGHTNESS) + if self._features & SUPPORT_RGB_COLOR: + self.chars.append(CHAR_HUE) + self.chars.append(CHAR_SATURATION) + + serv_light = add_preload_service(self, SERV_LIGHTBULB, self.chars) + self.char_on = serv_light.get_characteristic(CHAR_ON) + self.char_on.setter_callback = self.set_state + self.char_on.value = 0 + + if CHAR_BRIGHTNESS in self.chars: + self.char_brightness = serv_light \ + .get_characteristic(CHAR_BRIGHTNESS) + self.char_brightness.setter_callback = self.set_brightness + self.char_brightness.value = 0 + if CHAR_HUE in self.chars: + self.char_hue = serv_light.get_characteristic(CHAR_HUE) + self.char_hue.setter_callback = self.set_hue + self.char_hue.value = 0 + if CHAR_SATURATION in self.chars: + self.char_saturation = serv_light \ + .get_characteristic(CHAR_SATURATION) + self.char_saturation.setter_callback = self.set_saturation + self.char_saturation.value = 75 + + def set_state(self, value): + """Set state if call came from HomeKit.""" + if self._flag[CHAR_BRIGHTNESS]: + return + + _LOGGER.debug('%s: Set state to %d', self._entity_id, value) + self._flag[CHAR_ON] = True + + if value == 1: + self._hass.components.light.turn_on(self._entity_id) + elif value == 0: + self._hass.components.light.turn_off(self._entity_id) + + def set_brightness(self, value): + """Set brightness if call came from HomeKit.""" + _LOGGER.debug('%s: Set brightness to %d', self._entity_id, value) + self._flag[CHAR_BRIGHTNESS] = True + self._hass.components.light.turn_on( + self._entity_id, brightness_pct=value) + + def set_saturation(self, value): + """Set saturation if call came from HomeKit.""" + _LOGGER.debug('%s: Set saturation to %d', self._entity_id, value) + self._flag[CHAR_SATURATION] = True + self.color.saturation = value / 100 + self.set_color() + + def set_hue(self, value): + """Set hue if call came from HomeKit.""" + _LOGGER.debug('%s: Set hue to %d', self._entity_id, value) + self._flag[CHAR_HUE] = True + self.color.hue = value + self.set_color() + + def set_color(self): + """Set color if call came from HomeKit.""" + # Handle RGB Color + if self._features & SUPPORT_RGB_COLOR and self._flag[CHAR_HUE] and \ + self._flag[CHAR_SATURATION]: + color = self.color.calc_hsv_to_rgb() + _LOGGER.debug('%s: Set rgb_color to %s', self._entity_id, color) + self._flag.update({ + CHAR_HUE: False, CHAR_SATURATION: False, RGB_COLOR: True}) + self._hass.components.light.turn_on( + self._entity_id, rgb_color=color) + + def update_state(self, entity_id=None, old_state=None, new_state=None): + """Update light after state change.""" + if not new_state: + return + + # Handle State + state = new_state.state + if not self._flag[CHAR_ON] and state in [STATE_ON, STATE_OFF] and \ + self.char_on.value != (state == STATE_ON): + self.char_on.set_value(state == STATE_ON, should_callback=False) + self._flag[CHAR_ON] = False + + # Handle Brightness + if CHAR_BRIGHTNESS in self.chars: + brightness = new_state.attributes.get(ATTR_BRIGHTNESS) + if not self._flag[CHAR_BRIGHTNESS] and isinstance(brightness, int): + brightness = round(brightness / 255 * 100, 0) + if self.char_brightness.value != brightness: + self.char_brightness.set_value(brightness, + should_callback=False) + self._flag[CHAR_BRIGHTNESS] = False + + # Handle RGB Color + if CHAR_SATURATION in self.chars and CHAR_HUE in self.chars: + rgb_color = new_state.attributes.get(ATTR_RGB_COLOR) + if not self._flag[RGB_COLOR] and \ + isinstance(rgb_color, (list, tuple)) and \ + list(rgb_color) != self.color.calc_hsv_to_rgb(): + hue, saturation = Color.calc_rgb_to_hsv(rgb_color) + self.char_hue.set_value(hue, should_callback=False) + self.char_saturation.set_value(saturation, + should_callback=False) + self._flag[RGB_COLOR] = False diff --git a/homeassistant/components/homekit/type_security_systems.py b/homeassistant/components/homekit/type_security_systems.py index 02742acb75d..1d47160f9d2 100644 --- a/homeassistant/components/homekit/type_security_systems.py +++ b/homeassistant/components/homekit/type_security_systems.py @@ -5,7 +5,6 @@ from homeassistant.const import ( STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED, ATTR_ENTITY_ID, ATTR_CODE) -from homeassistant.helpers.event import async_track_state_change from . import TYPES from .accessories import HomeAccessory, add_preload_service @@ -50,14 +49,6 @@ class SecuritySystem(HomeAccessory): self.char_target_state.setter_callback = self.set_security_state - def run(self): - """Method called be object after driver is started.""" - state = self._hass.states.get(self._entity_id) - self.update_security_state(new_state=state) - - async_track_state_change(self._hass, self._entity_id, - self.update_security_state) - def set_security_state(self, value): """Move security state to value if call came from HomeKit.""" _LOGGER.debug('%s: Set security state to %d', @@ -69,8 +60,7 @@ class SecuritySystem(HomeAccessory): params = {ATTR_ENTITY_ID: self._entity_id, ATTR_CODE: self._alarm_code} self._hass.services.call('alarm_control_panel', service, params) - def update_security_state(self, entity_id=None, - old_state=None, new_state=None): + def update_state(self, entity_id=None, old_state=None, new_state=None): """Update security state after state changed.""" if new_state is None: return diff --git a/homeassistant/components/homekit/type_sensors.py b/homeassistant/components/homekit/type_sensors.py index 286862343f4..759fda08a02 100644 --- a/homeassistant/components/homekit/type_sensors.py +++ b/homeassistant/components/homekit/type_sensors.py @@ -3,13 +3,13 @@ import logging from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, TEMP_FAHRENHEIT, TEMP_CELSIUS) -from homeassistant.helpers.event import async_track_state_change from . import TYPES from .accessories import ( HomeAccessory, add_preload_service, override_properties) from .const import ( - SERV_TEMPERATURE_SENSOR, CHAR_CURRENT_TEMPERATURE, PROP_CELSIUS) + CATEGORY_SENSOR, SERV_HUMIDITY_SENSOR, SERV_TEMPERATURE_SENSOR, + CHAR_CURRENT_HUMIDITY, CHAR_CURRENT_TEMPERATURE, PROP_CELSIUS) _LOGGER = logging.getLogger(__name__) @@ -29,6 +29,14 @@ def calc_temperature(state, unit=TEMP_CELSIUS): return round((value - 32) / 1.8, 2) if unit == TEMP_FAHRENHEIT else value +def calc_humidity(state): + """Calculate humidity from state.""" + try: + return float(state) + except ValueError: + return None + + @TYPES.register('TemperatureSensor') class TemperatureSensor(HomeAccessory): """Generate a TemperatureSensor accessory for a temperature sensor. @@ -36,9 +44,9 @@ class TemperatureSensor(HomeAccessory): Sensor entity must return temperature in °C, °F. """ - def __init__(self, hass, entity_id, display_name, *args, **kwargs): + def __init__(self, hass, entity_id, name, *args, **kwargs): """Initialize a TemperatureSensor accessory object.""" - super().__init__(display_name, entity_id, 'SENSOR', *args, **kwargs) + super().__init__(name, entity_id, CATEGORY_SENSOR, *args, **kwargs) self._hass = hass self._entity_id = entity_id @@ -49,23 +57,42 @@ class TemperatureSensor(HomeAccessory): self.char_temp.value = 0 self.unit = None - def run(self): - """Method called be object after driver is started.""" - state = self._hass.states.get(self._entity_id) - self.update_temperature(new_state=state) - - async_track_state_change( - self._hass, self._entity_id, self.update_temperature) - - def update_temperature(self, entity_id=None, old_state=None, - new_state=None): + def update_state(self, entity_id=None, old_state=None, new_state=None): """Update temperature after state changed.""" if new_state is None: return unit = new_state.attributes[ATTR_UNIT_OF_MEASUREMENT] temperature = calc_temperature(new_state.state, unit) - if temperature is not None: - self.char_temp.set_value(temperature) + if temperature: + self.char_temp.set_value(temperature, should_callback=False) _LOGGER.debug('%s: Current temperature set to %d°C', self._entity_id, temperature) + + +@TYPES.register('HumiditySensor') +class HumiditySensor(HomeAccessory): + """Generate a HumiditySensor accessory as humidity sensor.""" + + def __init__(self, hass, entity_id, name, *args, **kwargs): + """Initialize a HumiditySensor accessory object.""" + super().__init__(name, entity_id, CATEGORY_SENSOR, *args, **kwargs) + + self._hass = hass + self._entity_id = entity_id + + serv_humidity = add_preload_service(self, SERV_HUMIDITY_SENSOR) + self.char_humidity = serv_humidity \ + .get_characteristic(CHAR_CURRENT_HUMIDITY) + self.char_humidity.value = 0 + + def update_state(self, entity_id=None, old_state=None, new_state=None): + """Update accessory after state change.""" + if new_state is None: + return + + humidity = calc_humidity(new_state.state) + if humidity: + self.char_humidity.set_value(humidity, should_callback=False) + _LOGGER.debug('%s: Current humidity set to %d%%', + self._entity_id, humidity) diff --git a/homeassistant/components/homekit/type_switches.py b/homeassistant/components/homekit/type_switches.py index 989bf4e19f5..fd3291ffe23 100644 --- a/homeassistant/components/homekit/type_switches.py +++ b/homeassistant/components/homekit/type_switches.py @@ -4,7 +4,6 @@ import logging from homeassistant.const import ( ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF, STATE_ON) from homeassistant.core import split_entity_id -from homeassistant.helpers.event import async_track_state_change from . import TYPES from .accessories import HomeAccessory, add_preload_service @@ -32,14 +31,6 @@ class Switch(HomeAccessory): self.char_on.value = False self.char_on.setter_callback = self.set_state - def run(self): - """Method called be object after driver is started.""" - state = self._hass.states.get(self._entity_id) - self.update_state(new_state=state) - - async_track_state_change(self._hass, self._entity_id, - self.update_state) - def set_state(self, value): """Move switch state to value if call came from HomeKit.""" _LOGGER.debug('%s: Set switch state to %s', diff --git a/homeassistant/components/homekit/type_thermostats.py b/homeassistant/components/homekit/type_thermostats.py index 6e720c2214e..b73b492ba74 100644 --- a/homeassistant/components/homekit/type_thermostats.py +++ b/homeassistant/components/homekit/type_thermostats.py @@ -8,7 +8,6 @@ from homeassistant.components.climate import ( STATE_HEAT, STATE_COOL, STATE_AUTO) from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS, TEMP_FAHRENHEIT) -from homeassistant.helpers.event import async_track_state_change from . import TYPES from .accessories import HomeAccessory, add_preload_service @@ -96,14 +95,6 @@ class Thermostat(HomeAccessory): self.char_cooling_thresh_temp = None self.char_heating_thresh_temp = None - def run(self): - """Method called be object after driver is started.""" - state = self._hass.states.get(self._entity_id) - self.update_thermostat(new_state=state) - - async_track_state_change(self._hass, self._entity_id, - self.update_thermostat) - def set_heat_cool(self, value): """Move operation mode to value if call came from HomeKit.""" if value in HC_HOMEKIT_TO_HASS: @@ -142,8 +133,7 @@ class Thermostat(HomeAccessory): self._hass.components.climate.set_temperature( temperature=value, entity_id=self._entity_id) - def update_thermostat(self, entity_id=None, - old_state=None, new_state=None): + def update_state(self, entity_id=None, old_state=None, new_state=None): """Update security state after state changed.""" if new_state is None: return diff --git a/tests/components/homekit/test_get_accessories.py b/tests/components/homekit/test_get_accessories.py index 6e1c67cf282..e6dbe1ff729 100644 --- a/tests/components/homekit/test_get_accessories.py +++ b/tests/components/homekit/test_get_accessories.py @@ -53,6 +53,13 @@ class TestGetAccessories(unittest.TestCase): {ATTR_UNIT_OF_MEASUREMENT: TEMP_FAHRENHEIT}) get_accessory(None, state, 2, {}) + def test_sensor_humidity(self): + """Test humidity sensor with % as unit.""" + with patch.dict(TYPES, {'HumiditySensor': self.mock_type}): + state = State('sensor.humidity', '20', + {ATTR_UNIT_OF_MEASUREMENT: '%'}) + get_accessory(None, state, 2, {}) + def test_cover_set_position(self): """Test cover with support for set_cover_position.""" with patch.dict(TYPES, {'WindowCovering': self.mock_type}): @@ -81,6 +88,12 @@ class TestGetAccessories(unittest.TestCase): self.assertEqual( self.mock_type.call_args[0][-1], False) # support_auto + def test_light(self): + """Test light devices.""" + with patch.dict(TYPES, {'Light': self.mock_type}): + state = State('light.test', 'on') + get_accessory(None, state, 2, {}) + def test_climate_support_auto(self): """Test climate devices with support for auto mode.""" with patch.dict(TYPES, {'Thermostat': self.mock_type}): diff --git a/tests/components/homekit/test_type_lights.py b/tests/components/homekit/test_type_lights.py new file mode 100644 index 00000000000..0e102c53860 --- /dev/null +++ b/tests/components/homekit/test_type_lights.py @@ -0,0 +1,160 @@ +"""Test different accessory types: Lights.""" +import unittest + +from homeassistant.core import callback +from homeassistant.components.homekit.type_lights import Light, Color +from homeassistant.components.light import ( + DOMAIN, ATTR_BRIGHTNESS, ATTR_BRIGHTNESS_PCT, ATTR_RGB_COLOR, + SUPPORT_BRIGHTNESS, SUPPORT_RGB_COLOR) +from homeassistant.const import ( + ATTR_DOMAIN, ATTR_ENTITY_ID, ATTR_SERVICE, ATTR_SERVICE_DATA, + ATTR_SUPPORTED_FEATURES, EVENT_CALL_SERVICE, SERVICE_TURN_ON, + SERVICE_TURN_OFF, STATE_ON, STATE_OFF, STATE_UNKNOWN) + +from tests.common import get_test_home_assistant + + +def test_calc_hsv_to_rgb(): + """Test conversion hsv to rgb.""" + color = Color(43, 23 / 100) + assert color.calc_hsv_to_rgb() == [255, 238, 196] + + color.hue, color.saturation = (79, 12 / 100) + assert color.calc_hsv_to_rgb() == [245, 255, 224] + + color.hue, color.saturation = (177, 2 / 100) + assert color.calc_hsv_to_rgb() == [250, 255, 255] + + color.hue, color.saturation = (212, 26 / 100) + assert color.calc_hsv_to_rgb() == [189, 220, 255] + + color.hue, color.saturation = (271, 93 / 100) + assert color.calc_hsv_to_rgb() == [140, 18, 255] + + color.hue, color.saturation = (355, 100 / 100) + assert color.calc_hsv_to_rgb() == [255, 0, 21] + + +def test_calc_rgb_to_hsv(): + """Test conversion rgb to hsv.""" + assert Color.calc_rgb_to_hsv([255, 0, 21]) == (355, 100) + assert Color.calc_rgb_to_hsv([245, 255, 224]) == (79, 12) + assert Color.calc_rgb_to_hsv([189, 220, 255]) == (212, 26) + + +class TestHomekitLights(unittest.TestCase): + """Test class for all accessory types regarding lights.""" + + def setUp(self): + """Setup things to be run when tests are started.""" + self.hass = get_test_home_assistant() + self.events = [] + + @callback + def record_event(event): + """Track called event.""" + self.events.append(event) + + self.hass.bus.listen(EVENT_CALL_SERVICE, record_event) + + def tearDown(self): + """Stop down everything that was started.""" + self.hass.stop() + + def test_light_basic(self): + """Test light with char state.""" + entity_id = 'light.demo' + self.hass.states.set(entity_id, STATE_ON, + {ATTR_SUPPORTED_FEATURES: 0}) + acc = Light(self.hass, entity_id, 'Light', aid=2) + self.assertEqual(acc.aid, 2) + self.assertEqual(acc.category, 5) # Lightbulb + self.assertEqual(acc.char_on.value, 0) + + acc.run() + self.hass.block_till_done() + self.assertEqual(acc.char_on.value, 1) + + self.hass.states.set(entity_id, STATE_OFF, + {ATTR_SUPPORTED_FEATURES: 0}) + self.hass.block_till_done() + self.assertEqual(acc.char_on.value, 0) + + self.hass.states.set(entity_id, STATE_UNKNOWN) + self.hass.block_till_done() + self.assertEqual(acc.char_on.value, 0) + + # Set from HomeKit + acc.char_on.set_value(True) + self.hass.block_till_done() + self.assertEqual( + self.events[0].data[ATTR_DOMAIN], DOMAIN) + self.assertEqual( + self.events[0].data[ATTR_SERVICE], SERVICE_TURN_ON) + + acc.char_on.set_value(False) + self.hass.block_till_done() + self.assertEqual( + self.events[1].data[ATTR_DOMAIN], DOMAIN) + self.assertEqual( + self.events[1].data[ATTR_SERVICE], SERVICE_TURN_OFF) + + # Remove entity + self.hass.states.remove(entity_id) + self.hass.block_till_done() + + def test_light_brightness(self): + """Test light with brightness.""" + entity_id = 'light.demo' + self.hass.states.set(entity_id, STATE_ON, { + ATTR_SUPPORTED_FEATURES: SUPPORT_BRIGHTNESS, ATTR_BRIGHTNESS: 255}) + acc = Light(self.hass, entity_id, 'Light', aid=2) + self.assertEqual(acc.char_brightness.value, 0) + + acc.run() + self.hass.block_till_done() + self.assertEqual(acc.char_brightness.value, 100) + + self.hass.states.set(entity_id, STATE_ON, {ATTR_BRIGHTNESS: 102}) + self.hass.block_till_done() + self.assertEqual(acc.char_brightness.value, 40) + + # Set from HomeKit + acc.char_brightness.set_value(20) + acc.char_on.set_value(1) + self.hass.block_till_done() + self.assertEqual( + self.events[0].data[ATTR_DOMAIN], DOMAIN) + self.assertEqual( + self.events[0].data[ATTR_SERVICE], SERVICE_TURN_ON) + print(self.events[0].data) + self.assertEqual( + self.events[0].data[ATTR_SERVICE_DATA], { + ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS_PCT: 20}) + + def test_light_rgb_color(self): + """Test light with rgb_color.""" + entity_id = 'light.demo' + self.hass.states.set(entity_id, STATE_ON, { + ATTR_SUPPORTED_FEATURES: SUPPORT_RGB_COLOR, + ATTR_RGB_COLOR: (120, 20, 300)}) + acc = Light(self.hass, entity_id, 'Light', aid=2) + self.assertEqual(acc.char_hue.value, 0) + self.assertEqual(acc.char_saturation.value, 75) + + acc.run() + self.hass.block_till_done() + self.assertEqual(acc.char_hue.value, 261) + self.assertEqual(acc.char_saturation.value, 93) + + # Set from HomeKit + acc.char_hue.set_value(145) + acc.char_saturation.set_value(75) + self.hass.block_till_done() + self.assertEqual( + self.events[0].data[ATTR_DOMAIN], DOMAIN) + self.assertEqual( + self.events[0].data[ATTR_SERVICE], SERVICE_TURN_ON) + self.assertEqual( + self.events[0].data[ATTR_SERVICE_DATA], { + ATTR_ENTITY_ID: entity_id, ATTR_RGB_COLOR: [64, 255, 143]}) diff --git a/tests/components/homekit/test_type_sensors.py b/tests/components/homekit/test_type_sensors.py index f9a14f6b8cf..b533c896019 100644 --- a/tests/components/homekit/test_type_sensors.py +++ b/tests/components/homekit/test_type_sensors.py @@ -3,7 +3,7 @@ import unittest from homeassistant.components.homekit.const import PROP_CELSIUS from homeassistant.components.homekit.type_sensors import ( - TemperatureSensor, calc_temperature) + TemperatureSensor, HumiditySensor, calc_temperature, calc_humidity) from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, STATE_UNKNOWN, TEMP_CELSIUS, TEMP_FAHRENHEIT) @@ -22,6 +22,15 @@ def test_calc_temperature(): assert calc_temperature('-20.6', TEMP_FAHRENHEIT) == -29.22 +def test_calc_humidity(): + """Test if humidity is a integer.""" + assert calc_humidity(STATE_UNKNOWN) is None + assert calc_humidity('test') is None + + assert calc_humidity('20') == 20 + assert calc_humidity('75.2') == 75.2 + + class TestHomekitSensors(unittest.TestCase): """Test class for all accessory types regarding sensors.""" @@ -60,3 +69,23 @@ class TestHomekitSensors(unittest.TestCase): {ATTR_UNIT_OF_MEASUREMENT: TEMP_FAHRENHEIT}) self.hass.block_till_done() self.assertEqual(acc.char_temp.value, 24) + + def test_humidity(self): + """Test if accessory is updated after state change.""" + entity_id = 'sensor.humidity' + + acc = HumiditySensor(self.hass, entity_id, 'Humidity', aid=2) + acc.run() + + self.assertEqual(acc.aid, 2) + self.assertEqual(acc.category, 10) # Sensor + + self.assertEqual(acc.char_humidity.value, 0) + + self.hass.states.set(entity_id, STATE_UNKNOWN, + {ATTR_UNIT_OF_MEASUREMENT: "%"}) + self.hass.block_till_done() + + self.hass.states.set(entity_id, '20', {ATTR_UNIT_OF_MEASUREMENT: "%"}) + self.hass.block_till_done() + self.assertEqual(acc.char_humidity.value, 20) From 0deef34881d7f8e46b5102e794e35bd8c110fef3 Mon Sep 17 00:00:00 2001 From: Fabien Piuzzi Date: Fri, 16 Mar 2018 03:50:58 +0100 Subject: [PATCH 109/220] Adding Foobot device sensor (#12417) * Added Foobot device sensor * Added error handling tests * Corrections after PR review. * Migrated to async/await syntax * lint fixes * stop raising HomeAssistantError * debug log for number of requests * Moved shared data between sensors from a class attribute to a separate class * Made test more async-aware disabled setup error test for now as it's not working * Working failure scenarios tests --- .coveragerc | 1 + homeassistant/components/sensor/foobot.py | 158 ++++++++++++++++++++++ requirements_all.txt | 3 + requirements_test_all.txt | 3 + script/gen_requirements_all.py | 1 + tests/components/sensor/test_foobot.py | 81 +++++++++++ tests/fixtures/foobot_data.json | 34 +++++ tests/fixtures/foobot_devices.json | 8 ++ 8 files changed, 289 insertions(+) create mode 100644 homeassistant/components/sensor/foobot.py create mode 100644 tests/components/sensor/test_foobot.py create mode 100644 tests/fixtures/foobot_data.json create mode 100644 tests/fixtures/foobot_devices.json diff --git a/.coveragerc b/.coveragerc index 5fd43d5aec7..b0f85b14c06 100644 --- a/.coveragerc +++ b/.coveragerc @@ -578,6 +578,7 @@ omit = homeassistant/components/sensor/fitbit.py homeassistant/components/sensor/fixer.py homeassistant/components/sensor/folder.py + homeassistant/components/sensor/foobot.py homeassistant/components/sensor/fritzbox_callmonitor.py homeassistant/components/sensor/fritzbox_netmonitor.py homeassistant/components/sensor/gearbest.py diff --git a/homeassistant/components/sensor/foobot.py b/homeassistant/components/sensor/foobot.py new file mode 100644 index 00000000000..8f65a335872 --- /dev/null +++ b/homeassistant/components/sensor/foobot.py @@ -0,0 +1,158 @@ +""" +Support for the Foobot indoor air quality monitor. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.foobot/ +""" +import asyncio +import logging +from datetime import timedelta + +import aiohttp +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.exceptions import PlatformNotReady +from homeassistant.const import ( + ATTR_TIME, ATTR_TEMPERATURE, CONF_TOKEN, CONF_USERNAME, TEMP_CELSIUS) +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.config_validation import PLATFORM_SCHEMA +from homeassistant.helpers.entity import Entity +from homeassistant.util import Throttle + + +REQUIREMENTS = ['foobot_async==0.3.0'] + +_LOGGER = logging.getLogger(__name__) + +ATTR_HUMIDITY = 'humidity' +ATTR_PM2_5 = 'PM2.5' +ATTR_CARBON_DIOXIDE = 'CO2' +ATTR_VOLATILE_ORGANIC_COMPOUNDS = 'VOC' +ATTR_FOOBOT_INDEX = 'index' + +SENSOR_TYPES = {'time': [ATTR_TIME, 's'], + 'pm': [ATTR_PM2_5, 'µg/m3', 'mdi:cloud'], + 'tmp': [ATTR_TEMPERATURE, TEMP_CELSIUS, 'mdi:thermometer'], + 'hum': [ATTR_HUMIDITY, '%', 'mdi:water-percent'], + 'co2': [ATTR_CARBON_DIOXIDE, 'ppm', + 'mdi:periodic-table-co2'], + 'voc': [ATTR_VOLATILE_ORGANIC_COMPOUNDS, 'ppb', + 'mdi:cloud'], + 'allpollu': [ATTR_FOOBOT_INDEX, '%', 'mdi:percent']} + +SCAN_INTERVAL = timedelta(minutes=10) +PARALLEL_UPDATES = 1 + +TIMEOUT = 10 + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_TOKEN): cv.string, + vol.Required(CONF_USERNAME): cv.string, +}) + + +async def async_setup_platform(hass, config, async_add_devices, + discovery_info=None): + """Set up the devices associated with the account.""" + from foobot_async import FoobotClient + + token = config.get(CONF_TOKEN) + username = config.get(CONF_USERNAME) + + client = FoobotClient(token, username, + async_get_clientsession(hass), + timeout=TIMEOUT) + dev = [] + try: + devices = await client.get_devices() + _LOGGER.debug("The following devices were found: %s", devices) + for device in devices: + foobot_data = FoobotData(client, device['uuid']) + for sensor_type in SENSOR_TYPES: + if sensor_type == 'time': + continue + foobot_sensor = FoobotSensor(foobot_data, device, sensor_type) + dev.append(foobot_sensor) + except (aiohttp.client_exceptions.ClientConnectorError, + asyncio.TimeoutError, FoobotClient.TooManyRequests, + FoobotClient.InternalError): + _LOGGER.exception('Failed to connect to foobot servers.') + raise PlatformNotReady + except FoobotClient.ClientError: + _LOGGER.error('Failed to fetch data from foobot servers.') + return + async_add_devices(dev, True) + + +class FoobotSensor(Entity): + """Implementation of a Foobot sensor.""" + + def __init__(self, data, device, sensor_type): + """Initialize the sensor.""" + self._uuid = device['uuid'] + self.foobot_data = data + self._name = 'Foobot {} {}'.format(device['name'], + SENSOR_TYPES[sensor_type][0]) + self.type = sensor_type + self._unit_of_measurement = SENSOR_TYPES[sensor_type][1] + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def icon(self): + """Icon to use in the frontend.""" + return SENSOR_TYPES[self.type][2] + + @property + def state(self): + """Return the state of the device.""" + try: + data = self.foobot_data.data[self.type] + except(KeyError, TypeError): + data = None + return data + + @property + def unique_id(self): + """Return the unique id of this entity.""" + return "{}_{}".format(self._uuid, self.type) + + @property + def unit_of_measurement(self): + """Return the unit of measurement of this entity.""" + return self._unit_of_measurement + + async def async_update(self): + """Get the latest data.""" + await self.foobot_data.async_update() + + +class FoobotData(Entity): + """Get data from Foobot API.""" + + def __init__(self, client, uuid): + """Initialize the data object.""" + self._client = client + self._uuid = uuid + self.data = {} + + @Throttle(SCAN_INTERVAL) + async def async_update(self): + """Get the data from Foobot API.""" + interval = SCAN_INTERVAL.total_seconds() + try: + response = await self._client.get_last_data(self._uuid, + interval, + interval + 1) + except (aiohttp.client_exceptions.ClientConnectorError, + asyncio.TimeoutError, self._client.TooManyRequests, + self._client.InternalError): + _LOGGER.debug("Couldn't fetch data") + return False + _LOGGER.debug("The data response is: %s", response) + self.data = {k: round(v, 1) for k, v in response[0].items()} + return True diff --git a/requirements_all.txt b/requirements_all.txt index 608618eb166..f2919eb9bc3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -287,6 +287,9 @@ fixerio==0.1.1 # homeassistant.components.light.flux_led flux_led==0.21 +# homeassistant.components.sensor.foobot +foobot_async==0.3.0 + # homeassistant.components.notify.free_mobile freesms==0.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9def3a7b301..69b56eabc5e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -62,6 +62,9 @@ evohomeclient==0.2.5 # homeassistant.components.sensor.geo_rss_events feedparser==5.2.1 +# homeassistant.components.sensor.foobot +foobot_async==0.3.0 + # homeassistant.components.tts.google gTTS-token==1.1.1 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index a7704088e26..d8fc7b1ed60 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -47,6 +47,7 @@ TEST_REQUIREMENTS = ( 'ephem', 'evohomeclient', 'feedparser', + 'foobot_async', 'gTTS-token', 'HAP-python', 'ha-ffmpeg', diff --git a/tests/components/sensor/test_foobot.py b/tests/components/sensor/test_foobot.py new file mode 100644 index 00000000000..322f2b3f2a8 --- /dev/null +++ b/tests/components/sensor/test_foobot.py @@ -0,0 +1,81 @@ +"""The tests for the Foobot sensor platform.""" + +import re +import asyncio +from unittest.mock import MagicMock +import pytest + + +import homeassistant.components.sensor as sensor +from homeassistant.components.sensor import foobot +from homeassistant.const import (TEMP_CELSIUS) +from homeassistant.exceptions import PlatformNotReady +from homeassistant.setup import async_setup_component +from tests.common import load_fixture + +VALID_CONFIG = { + 'platform': 'foobot', + 'token': 'adfdsfasd', + 'username': 'example@example.com', +} + + +async def test_default_setup(hass, aioclient_mock): + """Test the default setup.""" + aioclient_mock.get(re.compile('api.foobot.io/v2/owner/.*'), + text=load_fixture('foobot_devices.json')) + aioclient_mock.get(re.compile('api.foobot.io/v2/device/.*'), + text=load_fixture('foobot_data.json')) + assert await async_setup_component(hass, sensor.DOMAIN, + {'sensor': VALID_CONFIG}) + + metrics = {'co2': ['1232.0', 'ppm'], + 'temperature': ['21.1', TEMP_CELSIUS], + 'humidity': ['49.5', '%'], + 'pm25': ['144.8', 'µg/m3'], + 'voc': ['340.7', 'ppb'], + 'index': ['138.9', '%']} + + for name, value in metrics.items(): + state = hass.states.get('sensor.foobot_happybot_%s' % name) + assert state.state == value[0] + assert state.attributes.get('unit_of_measurement') == value[1] + + +async def test_setup_timeout_error(hass, aioclient_mock): + """Expected failures caused by a timeout in API response.""" + fake_async_add_devices = MagicMock() + + aioclient_mock.get(re.compile('api.foobot.io/v2/owner/.*'), + exc=asyncio.TimeoutError()) + with pytest.raises(PlatformNotReady): + await foobot.async_setup_platform(hass, {'sensor': VALID_CONFIG}, + fake_async_add_devices) + + +async def test_setup_permanent_error(hass, aioclient_mock): + """Expected failures caused by permanent errors in API response.""" + fake_async_add_devices = MagicMock() + + errors = [400, 401, 403] + for error in errors: + aioclient_mock.get(re.compile('api.foobot.io/v2/owner/.*'), + status=error) + result = await foobot.async_setup_platform(hass, + {'sensor': VALID_CONFIG}, + fake_async_add_devices) + assert result is None + + +async def test_setup_temporary_error(hass, aioclient_mock): + """Expected failures caused by temporary errors in API response.""" + fake_async_add_devices = MagicMock() + + errors = [429, 500] + for error in errors: + aioclient_mock.get(re.compile('api.foobot.io/v2/owner/.*'), + status=error) + with pytest.raises(PlatformNotReady): + await foobot.async_setup_platform(hass, + {'sensor': VALID_CONFIG}, + fake_async_add_devices) diff --git a/tests/fixtures/foobot_data.json b/tests/fixtures/foobot_data.json new file mode 100644 index 00000000000..93518614c42 --- /dev/null +++ b/tests/fixtures/foobot_data.json @@ -0,0 +1,34 @@ +{ + "uuid": "32463564765421243", + "start": 1518134963, + "end": 1518134963, + "sensors": [ + "time", + "pm", + "tmp", + "hum", + "co2", + "voc", + "allpollu" + ], + "units": [ + "s", + "ugm3", + "C", + "pc", + "ppm", + "ppb", + "%" + ], + "datapoints": [ + [ + 1518134963, + 144.76668, + 21.064333, + 49.474, + 1232.0, + 340.66666, + 138.93651 + ] + ] +} diff --git a/tests/fixtures/foobot_devices.json b/tests/fixtures/foobot_devices.json new file mode 100644 index 00000000000..fffc8e151cc --- /dev/null +++ b/tests/fixtures/foobot_devices.json @@ -0,0 +1,8 @@ +[ + { + "uuid": "231425657665645342", + "userId": 6545342, + "mac": "A2D3F1", + "name": "Happybot" + } +] From b1079cb49390cf552155c37f196d2df7eb430a75 Mon Sep 17 00:00:00 2001 From: karlkar Date: Fri, 16 Mar 2018 04:30:41 +0100 Subject: [PATCH 110/220] Fix for not setting up the camera if it is offline during setup phase (#13082) * Fix for not setting up the camera if it is offline during setup phase * async/await and modified service creation * Properly handle not supported PTZ * setup platform made synchronous as ONVIFService constructors do I/O * Fix intendation issue --- homeassistant/components/camera/onvif.py | 113 +++++++++++++++-------- 1 file changed, 75 insertions(+), 38 deletions(-) diff --git a/homeassistant/components/camera/onvif.py b/homeassistant/components/camera/onvif.py index d48f06539f4..3ae47ba5dee 100644 --- a/homeassistant/components/camera/onvif.py +++ b/homeassistant/components/camera/onvif.py @@ -6,6 +6,7 @@ https://home-assistant.io/components/camera.onvif/ """ import asyncio import logging +import os import voluptuous as vol @@ -103,92 +104,128 @@ class ONVIFHassCamera(Camera): def __init__(self, hass, config): """Initialize a ONVIF camera.""" - from onvif import ONVIFCamera, exceptions super().__init__() + import onvif + self._username = config.get(CONF_USERNAME) + self._password = config.get(CONF_PASSWORD) + self._host = config.get(CONF_HOST) + self._port = config.get(CONF_PORT) self._name = config.get(CONF_NAME) self._ffmpeg_arguments = config.get(CONF_EXTRA_ARGUMENTS) + self._profile_index = config.get(CONF_PROFILE) self._input = None - camera = None + self._media_service = \ + onvif.ONVIFService('http://{}:{}/onvif/device_service'.format( + self._host, self._port), + self._username, self._password, + '{}/wsdl/media.wsdl'.format(os.path.dirname( + onvif.__file__))) + + self._ptz_service = \ + onvif.ONVIFService('http://{}:{}/onvif/device_service'.format( + self._host, self._port), + self._username, self._password, + '{}/wsdl/ptz.wsdl'.format(os.path.dirname( + onvif.__file__))) + + def obtain_input_uri(self): + """Set the input uri for the camera.""" + from onvif import exceptions + _LOGGER.debug("Connecting with ONVIF Camera: %s on port %s", + self._host, self._port) + try: - _LOGGER.debug("Connecting with ONVIF Camera: %s on port %s", - config.get(CONF_HOST), config.get(CONF_PORT)) - camera = ONVIFCamera( - config.get(CONF_HOST), config.get(CONF_PORT), - config.get(CONF_USERNAME), config.get(CONF_PASSWORD) - ) - media_service = camera.create_media_service() - self._profiles = media_service.GetProfiles() - self._profile_index = config.get(CONF_PROFILE) - if self._profile_index >= len(self._profiles): + profiles = self._media_service.GetProfiles() + + if self._profile_index >= len(profiles): _LOGGER.warning("ONVIF Camera '%s' doesn't provide profile %d." " Using the last profile.", self._name, self._profile_index) self._profile_index = -1 - req = media_service.create_type('GetStreamUri') + + req = self._media_service.create_type('GetStreamUri') + # pylint: disable=protected-access - req.ProfileToken = self._profiles[self._profile_index]._token - self._input = media_service.GetStreamUri(req).Uri.replace( - 'rtsp://', 'rtsp://{}:{}@'.format( - config.get(CONF_USERNAME), - config.get(CONF_PASSWORD)), 1) + req.ProfileToken = profiles[self._profile_index]._token + uri_no_auth = self._media_service.GetStreamUri(req).Uri + uri_for_log = uri_no_auth.replace( + 'rtsp://', 'rtsp://:@', 1) + self._input = uri_no_auth.replace( + 'rtsp://', 'rtsp://{}:{}@'.format(self._username, + self._password), 1) _LOGGER.debug( "ONVIF Camera Using the following URL for %s: %s", - self._name, self._input) - except Exception as err: - _LOGGER.error("Unable to communicate with ONVIF Camera: %s", err) - raise - try: - self._ptz = camera.create_ptz_service() + self._name, uri_for_log) + # we won't need the media service anymore + self._media_service = None except exceptions.ONVIFError as err: - self._ptz = None - _LOGGER.warning("Unable to setup PTZ for ONVIF Camera: %s", err) + _LOGGER.debug("Couldn't setup camera '%s'. Error: %s", + self._name, err) + return def perform_ptz(self, pan, tilt, zoom): """Perform a PTZ action on the camera.""" - if self._ptz: + from onvif import exceptions + if self._ptz_service: pan_val = 1 if pan == DIR_RIGHT else -1 if pan == DIR_LEFT else 0 tilt_val = 1 if tilt == DIR_UP else -1 if tilt == DIR_DOWN else 0 zoom_val = 1 if zoom == ZOOM_IN else -1 if zoom == ZOOM_OUT else 0 req = {"Velocity": { "PanTilt": {"_x": pan_val, "_y": tilt_val}, "Zoom": {"_x": zoom_val}}} - self._ptz.ContinuousMove(req) + try: + self._ptz_service.ContinuousMove(req) + except exceptions.ONVIFError as err: + if "Bad Request" in err.reason: + self._ptz_service = None + _LOGGER.debug("Camera '%s' doesn't support PTZ.", + self._name) + else: + _LOGGER.debug("Camera '%s' doesn't support PTZ.", self._name) - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Callback when entity is added to hass.""" if ONVIF_DATA not in self.hass.data: self.hass.data[ONVIF_DATA] = {} self.hass.data[ONVIF_DATA][ENTITIES] = [] self.hass.data[ONVIF_DATA][ENTITIES].append(self) - @asyncio.coroutine - def async_camera_image(self): + async def async_camera_image(self): """Return a still image response from the camera.""" from haffmpeg import ImageFrame, IMAGE_JPEG + + if not self._input: + await self.hass.async_add_job(self.obtain_input_uri) + if not self._input: + return None + ffmpeg = ImageFrame( self.hass.data[DATA_FFMPEG].binary, loop=self.hass.loop) - image = yield from asyncio.shield(ffmpeg.get_image( + image = await asyncio.shield(ffmpeg.get_image( self._input, output_format=IMAGE_JPEG, extra_cmd=self._ffmpeg_arguments), loop=self.hass.loop) return image - @asyncio.coroutine - def handle_async_mjpeg_stream(self, request): + async def handle_async_mjpeg_stream(self, request): """Generate an HTTP MJPEG stream from the camera.""" from haffmpeg import CameraMjpeg + if not self._input: + await self.hass.async_add_job(self.obtain_input_uri) + if not self._input: + return None + stream = CameraMjpeg(self.hass.data[DATA_FFMPEG].binary, loop=self.hass.loop) - yield from stream.open_camera( + await stream.open_camera( self._input, extra_cmd=self._ffmpeg_arguments) - yield from async_aiohttp_proxy_stream( + await async_aiohttp_proxy_stream( self.hass, request, stream, 'multipart/x-mixed-replace;boundary=ffserver') - yield from stream.close() + await stream.close() @property def name(self): From 99f7e2bd979e20ac7847a1b1c727452b06177f66 Mon Sep 17 00:00:00 2001 From: BioSehnsucht Date: Thu, 15 Mar 2018 22:36:03 -0500 Subject: [PATCH 111/220] Added Stride notification component (#13221) * Added Stride notification component * Fix trailing whitespace in Stride notify * More whitespace fixes and rogue comment for Stride notify * More whitespace fixing for Stride notify * Correcting hanging indents for Stride notify --- .coveragerc | 1 + homeassistant/components/notify/stride.py | 102 ++++++++++++++++++++++ requirements_all.txt | 3 + 3 files changed, 106 insertions(+) create mode 100644 homeassistant/components/notify/stride.py diff --git a/.coveragerc b/.coveragerc index b0f85b14c06..4da5343bf4f 100644 --- a/.coveragerc +++ b/.coveragerc @@ -519,6 +519,7 @@ omit = homeassistant/components/notify/sendgrid.py homeassistant/components/notify/simplepush.py homeassistant/components/notify/slack.py + homeassistant/components/notify/stride.py homeassistant/components/notify/smtp.py homeassistant/components/notify/synology_chat.py homeassistant/components/notify/syslog.py diff --git a/homeassistant/components/notify/stride.py b/homeassistant/components/notify/stride.py new file mode 100644 index 00000000000..f31e50a5886 --- /dev/null +++ b/homeassistant/components/notify/stride.py @@ -0,0 +1,102 @@ +""" +Stride platform for notify component. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/notify.stride/ +""" +import logging + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.notify import ( + ATTR_TARGET, ATTR_DATA, PLATFORM_SCHEMA, BaseNotificationService) +from homeassistant.const import CONF_TOKEN, CONF_ROOM + +REQUIREMENTS = ['pystride==0.1.7'] + +_LOGGER = logging.getLogger(__name__) + +CONF_PANEL = 'panel' +CONF_CLOUDID = 'cloudid' + +DEFAULT_PANEL = None + +VALID_PANELS = {'info', 'note', 'tip', 'warning', None} + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_CLOUDID): cv.string, + vol.Required(CONF_ROOM): cv.string, + vol.Required(CONF_TOKEN): cv.string, + vol.Optional(CONF_PANEL, default=DEFAULT_PANEL): vol.In(VALID_PANELS), +}) + + +def get_service(hass, config, discovery_info=None): + """Get the Stride notification service.""" + return StrideNotificationService( + config[CONF_TOKEN], config[CONF_ROOM], config[CONF_PANEL], + config[CONF_CLOUDID]) + + +class StrideNotificationService(BaseNotificationService): + """Implement the notification service for Stride.""" + + def __init__(self, token, default_room, default_panel, cloudid): + """Initialize the service.""" + self._token = token + self._default_room = default_room + self._default_panel = default_panel + self._cloudid = cloudid + + from stride import Stride + self._stride = Stride(self._cloudid, access_token=self._token) + + def send_message(self, message="", **kwargs): + """Send a message.""" + panel = self._default_panel + + if kwargs.get(ATTR_DATA) is not None: + data = kwargs.get(ATTR_DATA) + if ((data.get(CONF_PANEL) is not None) + and (data.get(CONF_PANEL) in VALID_PANELS)): + panel = data.get(CONF_PANEL) + + message_text = { + 'type': 'paragraph', + 'content': [ + { + 'type': 'text', + 'text': message + } + ] + } + panel_text = message_text + if panel is not None: + panel_text = { + 'type': 'panel', + 'attrs': + { + 'panelType': panel + }, + 'content': + [ + message_text, + ] + } + + message_doc = { + 'body': { + 'version': 1, + 'type': 'doc', + 'content': + [ + panel_text, + ] + } + } + + targets = kwargs.get(ATTR_TARGET, [self._default_room]) + + for target in targets: + self._stride.message_room(target, message_doc) diff --git a/requirements_all.txt b/requirements_all.txt index f2919eb9bc3..b30b77307c1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -891,6 +891,9 @@ pysma==0.2 # homeassistant.components.switch.snmp pysnmp==4.4.4 +# homeassistant.components.notify.stride +pystride==0.1.7 + # homeassistant.components.media_player.liveboxplaytv pyteleloisirs==3.3 From f6ae2d338d6223a8715fcb434ad75caad687340a Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Fri, 16 Mar 2018 11:38:44 +0100 Subject: [PATCH 112/220] Homekit: Use util functions for unit conversion (#13253) * Updated to util/color for conversion * Updated temperature sensor to use util/temperature conversion --- .../components/homekit/type_lights.py | 80 +++---------------- .../components/homekit/type_sensors.py | 3 +- tests/components/homekit/test_type_lights.py | 38 ++------- tests/components/homekit/test_type_sensors.py | 2 - 4 files changed, 17 insertions(+), 106 deletions(-) diff --git a/homeassistant/components/homekit/type_lights.py b/homeassistant/components/homekit/type_lights.py index 107ad1db1e4..db7172bef17 100644 --- a/homeassistant/components/homekit/type_lights.py +++ b/homeassistant/components/homekit/type_lights.py @@ -5,6 +5,7 @@ from homeassistant.components.light import ( ATTR_RGB_COLOR, ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, SUPPORT_RGB_COLOR) from homeassistant.const import ATTR_SUPPORTED_FEATURES, STATE_ON, STATE_OFF +from homeassistant.util.color import color_RGB_to_hsv, color_hsv_to_RGB from . import TYPES from .accessories import HomeAccessory, add_preload_service @@ -17,69 +18,6 @@ _LOGGER = logging.getLogger(__name__) RGB_COLOR = 'rgb_color' -class Color: - """Class to handle color conversions.""" - - # pylint: disable=invalid-name - - def __init__(self, hue=None, saturation=None): - """Initialize a new Color object.""" - self.hue = hue # [0, 360] - self.saturation = saturation # [0, 1] - - def calc_hsv_to_rgb(self): - """Convert hsv_color value to rgb_color.""" - if not self.hue or not self.saturation: - return [None] * 3 - - i = int(self.hue / 60) - f = self.hue / 60 - i - v = 1 - p = 1 - self.saturation - q = 1 - self.saturation * f - t = 1 - self.saturation * (1 - f) - - rgb = [] - if i in [0, 6]: - rgb = [v, t, p] - elif i == 1: - rgb = [q, v, p] - elif i == 2: - rgb = [p, v, t] - elif i == 3: - rgb = [p, q, v] - elif i == 4: - rgb = [t, p, v] - elif i == 5: - rgb = [v, p, q] - - return [round(c * 255) for c in rgb] - - @classmethod - def calc_rgb_to_hsv(cls, rgb_color): - """Convert a give rgb_color back to a hsv_color.""" - rgb_color = [c / 255 for c in rgb_color] - c_max = max(rgb_color) - c_min = min(rgb_color) - c_diff = c_max - c_min - r, g, b = rgb_color - - hue, saturation = 0, 0 - if c_max == r: - hue = 60 * (0 + (g - b) / c_diff) - elif c_max == g: - hue = 60 * (2 + (b - r) / c_diff) - elif c_max == b: - hue = 60 * (4 + (r - g) / c_diff) - - hue = round(hue + 360) if hue < 0 else round(hue) - - if c_max != 0: - saturation = round((c_max - c_min) / c_max * 100) - - return (hue, saturation) - - @TYPES.register('Light') class Light(HomeAccessory): """Generate a Light accessory for a light entity. @@ -97,8 +35,6 @@ class Light(HomeAccessory): CHAR_HUE: False, CHAR_SATURATION: False, RGB_COLOR: False} - self.color = Color() - self.chars = [] self._features = self._hass.states.get(self._entity_id) \ .attributes.get(ATTR_SUPPORTED_FEATURES) @@ -107,6 +43,8 @@ class Light(HomeAccessory): if self._features & SUPPORT_RGB_COLOR: self.chars.append(CHAR_HUE) self.chars.append(CHAR_SATURATION) + self._hue = None + self._saturation = None serv_light = add_preload_service(self, SERV_LIGHTBULB, self.chars) self.char_on = serv_light.get_characteristic(CHAR_ON) @@ -152,14 +90,14 @@ class Light(HomeAccessory): """Set saturation if call came from HomeKit.""" _LOGGER.debug('%s: Set saturation to %d', self._entity_id, value) self._flag[CHAR_SATURATION] = True - self.color.saturation = value / 100 + self._saturation = value self.set_color() def set_hue(self, value): """Set hue if call came from HomeKit.""" _LOGGER.debug('%s: Set hue to %d', self._entity_id, value) self._flag[CHAR_HUE] = True - self.color.hue = value + self._hue = value self.set_color() def set_color(self): @@ -167,7 +105,7 @@ class Light(HomeAccessory): # Handle RGB Color if self._features & SUPPORT_RGB_COLOR and self._flag[CHAR_HUE] and \ self._flag[CHAR_SATURATION]: - color = self.color.calc_hsv_to_rgb() + color = color_hsv_to_RGB(self._hue, self._saturation, 100) _LOGGER.debug('%s: Set rgb_color to %s', self._entity_id, color) self._flag.update({ CHAR_HUE: False, CHAR_SATURATION: False, RGB_COLOR: True}) @@ -199,10 +137,12 @@ class Light(HomeAccessory): # Handle RGB Color if CHAR_SATURATION in self.chars and CHAR_HUE in self.chars: rgb_color = new_state.attributes.get(ATTR_RGB_COLOR) + current_color = color_hsv_to_RGB(self._hue, self._saturation, 100)\ + if self._hue and self._saturation else [None] * 3 if not self._flag[RGB_COLOR] and \ isinstance(rgb_color, (list, tuple)) and \ - list(rgb_color) != self.color.calc_hsv_to_rgb(): - hue, saturation = Color.calc_rgb_to_hsv(rgb_color) + tuple(rgb_color) != current_color: + hue, saturation, _ = color_RGB_to_hsv(*rgb_color) self.char_hue.set_value(hue, should_callback=False) self.char_saturation.set_value(saturation, should_callback=False) diff --git a/homeassistant/components/homekit/type_sensors.py b/homeassistant/components/homekit/type_sensors.py index 759fda08a02..7575acb5c35 100644 --- a/homeassistant/components/homekit/type_sensors.py +++ b/homeassistant/components/homekit/type_sensors.py @@ -3,6 +3,7 @@ import logging from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, TEMP_FAHRENHEIT, TEMP_CELSIUS) +from homeassistant.util.temperature import fahrenheit_to_celsius from . import TYPES from .accessories import ( @@ -26,7 +27,7 @@ def calc_temperature(state, unit=TEMP_CELSIUS): except ValueError: return None - return round((value - 32) / 1.8, 2) if unit == TEMP_FAHRENHEIT else value + return fahrenheit_to_celsius(value) if unit == TEMP_FAHRENHEIT else value def calc_humidity(state): diff --git a/tests/components/homekit/test_type_lights.py b/tests/components/homekit/test_type_lights.py index 0e102c53860..83456f459cd 100644 --- a/tests/components/homekit/test_type_lights.py +++ b/tests/components/homekit/test_type_lights.py @@ -2,7 +2,7 @@ import unittest from homeassistant.core import callback -from homeassistant.components.homekit.type_lights import Light, Color +from homeassistant.components.homekit.type_lights import Light from homeassistant.components.light import ( DOMAIN, ATTR_BRIGHTNESS, ATTR_BRIGHTNESS_PCT, ATTR_RGB_COLOR, SUPPORT_BRIGHTNESS, SUPPORT_RGB_COLOR) @@ -14,34 +14,6 @@ from homeassistant.const import ( from tests.common import get_test_home_assistant -def test_calc_hsv_to_rgb(): - """Test conversion hsv to rgb.""" - color = Color(43, 23 / 100) - assert color.calc_hsv_to_rgb() == [255, 238, 196] - - color.hue, color.saturation = (79, 12 / 100) - assert color.calc_hsv_to_rgb() == [245, 255, 224] - - color.hue, color.saturation = (177, 2 / 100) - assert color.calc_hsv_to_rgb() == [250, 255, 255] - - color.hue, color.saturation = (212, 26 / 100) - assert color.calc_hsv_to_rgb() == [189, 220, 255] - - color.hue, color.saturation = (271, 93 / 100) - assert color.calc_hsv_to_rgb() == [140, 18, 255] - - color.hue, color.saturation = (355, 100 / 100) - assert color.calc_hsv_to_rgb() == [255, 0, 21] - - -def test_calc_rgb_to_hsv(): - """Test conversion rgb to hsv.""" - assert Color.calc_rgb_to_hsv([255, 0, 21]) == (355, 100) - assert Color.calc_rgb_to_hsv([245, 255, 224]) == (79, 12) - assert Color.calc_rgb_to_hsv([189, 220, 255]) == (212, 26) - - class TestHomekitLights(unittest.TestCase): """Test class for all accessory types regarding lights.""" @@ -137,15 +109,15 @@ class TestHomekitLights(unittest.TestCase): entity_id = 'light.demo' self.hass.states.set(entity_id, STATE_ON, { ATTR_SUPPORTED_FEATURES: SUPPORT_RGB_COLOR, - ATTR_RGB_COLOR: (120, 20, 300)}) + ATTR_RGB_COLOR: (120, 20, 255)}) acc = Light(self.hass, entity_id, 'Light', aid=2) self.assertEqual(acc.char_hue.value, 0) self.assertEqual(acc.char_saturation.value, 75) acc.run() self.hass.block_till_done() - self.assertEqual(acc.char_hue.value, 261) - self.assertEqual(acc.char_saturation.value, 93) + self.assertEqual(acc.char_hue.value, 265.532) + self.assertEqual(acc.char_saturation.value, 92.157) # Set from HomeKit acc.char_hue.set_value(145) @@ -157,4 +129,4 @@ class TestHomekitLights(unittest.TestCase): self.events[0].data[ATTR_SERVICE], SERVICE_TURN_ON) self.assertEqual( self.events[0].data[ATTR_SERVICE_DATA], { - ATTR_ENTITY_ID: entity_id, ATTR_RGB_COLOR: [64, 255, 143]}) + ATTR_ENTITY_ID: entity_id, ATTR_RGB_COLOR: (63, 255, 143)}) diff --git a/tests/components/homekit/test_type_sensors.py b/tests/components/homekit/test_type_sensors.py index b533c896019..551dfc6780d 100644 --- a/tests/components/homekit/test_type_sensors.py +++ b/tests/components/homekit/test_type_sensors.py @@ -17,9 +17,7 @@ def test_calc_temperature(): assert calc_temperature('20') == 20 assert calc_temperature('20.12', TEMP_CELSIUS) == 20.12 - assert calc_temperature('75.2', TEMP_FAHRENHEIT) == 24 - assert calc_temperature('-20.6', TEMP_FAHRENHEIT) == -29.22 def test_calc_humidity(): From 78144bc6dedea0bf53ff8f77f9f5f74ffab858e9 Mon Sep 17 00:00:00 2001 From: Teemu R Date: Fri, 16 Mar 2018 12:14:21 +0100 Subject: [PATCH 113/220] Use the first, not the last volume controller when multiple are available on songpal (#13222) * use the first, not the last volume controller * Do not mutate the list but simply pick the first by index --- homeassistant/components/media_player/songpal.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/media_player/songpal.py b/homeassistant/components/media_player/songpal.py index b1dc7df3319..e43f5951db7 100644 --- a/homeassistant/components/media_player/songpal.py +++ b/homeassistant/components/media_player/songpal.py @@ -154,7 +154,7 @@ class SongpalDevice(MediaPlayerDevice): _LOGGER.warning("Got %s volume controls, using the first one", volumes) - volume = volumes.pop() + volume = volumes[0] _LOGGER.debug("Current volume: %s", volume) self._volume_max = volume.maxVolume From f013619e6990181d35ce414f5a95667b96cbb1a8 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Fri, 16 Mar 2018 19:58:03 +0100 Subject: [PATCH 114/220] Xiaomi MiIO Switch: Power Strip support improved (#12917) * Xiaomi MiIO Switch: Power Strip support improved. * New service descriptions added. * Make hound happy. * Pylint fixed. * Use Async / await syntax. * Missed method fixed. * Make hound happy. * Don't abuse the system property supported_features anymore. * Check the correct method. * Refactoring. * Make hound happy. * pythion-miio version bumped. * Clean-up. * Unique id added. * Filter service calls. Device unavailable handling improved. --- homeassistant/components/switch/services.yaml | 31 +++ .../components/switch/xiaomi_miio.py | 260 ++++++++++++++---- 2 files changed, 239 insertions(+), 52 deletions(-) diff --git a/homeassistant/components/switch/services.yaml b/homeassistant/components/switch/services.yaml index f52b197d432..46b1237f57c 100644 --- a/homeassistant/components/switch/services.yaml +++ b/homeassistant/components/switch/services.yaml @@ -30,3 +30,34 @@ mysensors_send_ir_code: V_IR_SEND: description: IR code to send. example: '0xC284' + +xiaomi_miio_set_wifi_led_on: + description: Turn the wifi led on. + fields: + entity_id: + description: Name of the xiaomi miio entity. + example: 'switch.xiaomi_miio_device' +xiaomi_miio_set_wifi_led_off: + description: Turn the wifi led off. + fields: + entity_id: + description: Name of the xiaomi miio entity. + example: 'switch.xiaomi_miio_device' +xiaomi_miio_set_power_price: + description: Set the power price. + fields: + entity_id: + description: Name of the xiaomi miio entity. + example: 'switch.xiaomi_miio_device' + mode: + description: Power price, between 0 and 999. + example: 31 +xiaomi_miio_set_power_mode: + description: Set the power mode. + fields: + entity_id: + description: Name of the xiaomi miio entity. + example: 'switch.xiaomi_miio_device' + mode: + description: Power mode, valid values are 'normal' and 'green'. + example: 'green' diff --git a/homeassistant/components/switch/xiaomi_miio.py b/homeassistant/components/switch/xiaomi_miio.py index 1a8feb5811d..9f0f163df69 100644 --- a/homeassistant/components/switch/xiaomi_miio.py +++ b/homeassistant/components/switch/xiaomi_miio.py @@ -11,15 +11,19 @@ import logging import voluptuous as vol import homeassistant.helpers.config_validation as cv -from homeassistant.components.switch import (SwitchDevice, PLATFORM_SCHEMA, ) -from homeassistant.const import (CONF_NAME, CONF_HOST, CONF_TOKEN, ) +from homeassistant.components.switch import (SwitchDevice, PLATFORM_SCHEMA, + DOMAIN, ) +from homeassistant.const import (CONF_NAME, CONF_HOST, CONF_TOKEN, + ATTR_ENTITY_ID, ) from homeassistant.exceptions import PlatformNotReady _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = 'Xiaomi Miio Switch' +DATA_KEY = 'switch.xiaomi_miio' CONF_MODEL = 'model' +MODEL_POWER_STRIP_V2 = 'zimi.powerstrip.v2' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_HOST): cv.string, @@ -39,14 +43,63 @@ ATTR_POWER = 'power' ATTR_TEMPERATURE = 'temperature' ATTR_LOAD_POWER = 'load_power' ATTR_MODEL = 'model' +ATTR_MODE = 'mode' +ATTR_POWER_MODE = 'power_mode' +ATTR_WIFI_LED = 'wifi_led' +ATTR_POWER_PRICE = 'power_price' +ATTR_PRICE = 'price' + SUCCESS = ['ok'] +SUPPORT_SET_POWER_MODE = 1 +SUPPORT_SET_WIFI_LED = 2 +SUPPORT_SET_POWER_PRICE = 4 + +ADDITIONAL_SUPPORT_FLAGS_GENERIC = 0 + +ADDITIONAL_SUPPORT_FLAGS_POWER_STRIP_V1 = (SUPPORT_SET_POWER_MODE | + SUPPORT_SET_WIFI_LED | + SUPPORT_SET_POWER_PRICE) + +ADDITIONAL_SUPPORT_FLAGS_POWER_STRIP_V2 = (SUPPORT_SET_WIFI_LED | + SUPPORT_SET_POWER_PRICE) + +SERVICE_SET_WIFI_LED_ON = 'xiaomi_miio_set_wifi_led_on' +SERVICE_SET_WIFI_LED_OFF = 'xiaomi_miio_set_wifi_led_off' +SERVICE_SET_POWER_MODE = 'xiaomi_miio_set_power_mode' +SERVICE_SET_POWER_PRICE = 'xiaomi_miio_set_power_price' + +SERVICE_SCHEMA = vol.Schema({ + vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, +}) + +SERVICE_SCHEMA_POWER_MODE = SERVICE_SCHEMA.extend({ + vol.Required(ATTR_MODE): vol.All(vol.In(['green', 'normal'])), +}) + +SERVICE_SCHEMA_POWER_PRICE = SERVICE_SCHEMA.extend({ + vol.Required(ATTR_PRICE): vol.All(vol.Coerce(float), vol.Range(min=0)) +}) + +SERVICE_TO_METHOD = { + SERVICE_SET_WIFI_LED_ON: {'method': 'async_set_wifi_led_on'}, + SERVICE_SET_WIFI_LED_OFF: {'method': 'async_set_wifi_led_off'}, + SERVICE_SET_POWER_MODE: { + 'method': 'async_set_power_mode', + 'schema': SERVICE_SCHEMA_POWER_MODE}, + SERVICE_SET_POWER_PRICE: { + 'method': 'async_set_power_price', + 'schema': SERVICE_SCHEMA_POWER_PRICE}, +} + # pylint: disable=unused-argument -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_devices, discovery_info=None): +async def async_setup_platform(hass, config, async_add_devices, + discovery_info=None): """Set up the switch from config.""" from miio import Device, DeviceException + if DATA_KEY not in hass.data: + hass.data[DATA_KEY] = {} host = config.get(CONF_HOST) name = config.get(CONF_NAME) @@ -56,12 +109,14 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): _LOGGER.info("Initializing with host %s (token %s...)", host, token[:5]) devices = [] + unique_id = None if model is None: try: miio_device = Device(host, token) device_info = miio_device.info() model = device_info.model + unique_id = "{}-{}".format(model, device_info.mac_address) _LOGGER.info("%s %s %s detected", model, device_info.firmware_version, @@ -77,21 +132,24 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): # A switch device per channel will be created. for channel_usb in [True, False]: device = ChuangMiPlugV1Switch( - name, plug, model, channel_usb) + name, plug, model, unique_id, channel_usb) devices.append(device) + hass.data[DATA_KEY][host] = device elif model in ['qmi.powerstrip.v1', 'zimi.powerstrip.v2']: from miio import PowerStrip plug = PowerStrip(host, token) - device = XiaomiPowerStripSwitch(name, plug, model) + device = XiaomiPowerStripSwitch(name, plug, model, unique_id) devices.append(device) + hass.data[DATA_KEY][host] = device elif model in ['chuangmi.plug.m1', 'chuangmi.plug.v2']: from miio import Plug plug = Plug(host, token) - device = XiaomiPlugGenericSwitch(name, plug, model) + device = XiaomiPlugGenericSwitch(name, plug, model, unique_id) devices.append(device) + hass.data[DATA_KEY][host] = device else: _LOGGER.error( 'Unsupported device found! Please create an issue at ' @@ -101,22 +159,52 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): async_add_devices(devices, update_before_add=True) + async def async_service_handler(service): + """Map services to methods on XiaomiPlugGenericSwitch.""" + method = SERVICE_TO_METHOD.get(service.service) + params = {key: value for key, value in service.data.items() + if key != ATTR_ENTITY_ID} + entity_ids = service.data.get(ATTR_ENTITY_ID) + if entity_ids: + devices = [device for device in hass.data[DATA_KEY].values() if + device.entity_id in entity_ids] + else: + devices = hass.data[DATA_KEY].values() + + update_tasks = [] + for device in devices: + if not hasattr(device, method['method']): + continue + await getattr(device, method['method'])(**params) + update_tasks.append(device.async_update_ha_state(True)) + + if update_tasks: + await asyncio.wait(update_tasks, loop=hass.loop) + + for plug_service in SERVICE_TO_METHOD: + schema = SERVICE_TO_METHOD[plug_service].get('schema', SERVICE_SCHEMA) + hass.services.async_register( + DOMAIN, plug_service, async_service_handler, schema=schema) + class XiaomiPlugGenericSwitch(SwitchDevice): """Representation of a Xiaomi Plug Generic.""" - def __init__(self, name, plug, model): + def __init__(self, name, plug, model, unique_id): """Initialize the plug switch.""" self._name = name - self._icon = 'mdi:power-socket' - self._model = model - self._plug = plug + self._model = model + self._unique_id = unique_id + + self._icon = 'mdi:power-socket' + self._available = False self._state = None self._state_attrs = { ATTR_TEMPERATURE: None, ATTR_MODEL: self._model, } + self._additional_supported_features = ADDITIONAL_SUPPORT_FLAGS_GENERIC self._skip_update = False @property @@ -124,6 +212,11 @@ class XiaomiPlugGenericSwitch(SwitchDevice): """Poll the plug.""" return True + @property + def unique_id(self): + """Return an unique ID.""" + return self._unique_id + @property def name(self): """Return the name of the device if any.""" @@ -137,7 +230,7 @@ class XiaomiPlugGenericSwitch(SwitchDevice): @property def available(self): """Return true when state is known.""" - return self._state is not None + return self._available @property def device_state_attributes(self): @@ -149,12 +242,11 @@ class XiaomiPlugGenericSwitch(SwitchDevice): """Return true if switch is on.""" return self._state - @asyncio.coroutine - def _try_command(self, mask_error, func, *args, **kwargs): + async def _try_command(self, mask_error, func, *args, **kwargs): """Call a plug command handling error messages.""" from miio import DeviceException try: - result = yield from self.hass.async_add_job( + result = await self.hass.async_add_job( partial(func, *args, **kwargs)) _LOGGER.debug("Response received from plug: %s", result) @@ -162,30 +254,28 @@ class XiaomiPlugGenericSwitch(SwitchDevice): return result == SUCCESS except DeviceException as exc: _LOGGER.error(mask_error, exc) + self._available = False return False - @asyncio.coroutine - def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): """Turn the plug on.""" - result = yield from self._try_command( + result = await self._try_command( "Turning the plug on failed.", self._plug.on) if result: self._state = True self._skip_update = True - @asyncio.coroutine - def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs): """Turn the plug off.""" - result = yield from self._try_command( + result = await self._try_command( "Turning the plug off failed.", self._plug.off) if result: self._state = False self._skip_update = True - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Fetch state from the device.""" from miio import DeviceException @@ -195,34 +285,75 @@ class XiaomiPlugGenericSwitch(SwitchDevice): return try: - state = yield from self.hass.async_add_job(self._plug.status) + state = await self.hass.async_add_job(self._plug.status) _LOGGER.debug("Got new state: %s", state) + self._available = True self._state = state.is_on self._state_attrs.update({ ATTR_TEMPERATURE: state.temperature }) except DeviceException as ex: - self._state = None + self._available = False _LOGGER.error("Got exception while fetching the state: %s", ex) + async def async_set_wifi_led_on(self): + """Turn the wifi led on.""" + if self._additional_supported_features & SUPPORT_SET_WIFI_LED == 0: + return -class XiaomiPowerStripSwitch(XiaomiPlugGenericSwitch, SwitchDevice): + await self._try_command( + "Turning the wifi led on failed.", + self._plug.set_wifi_led, True) + + async def async_set_wifi_led_off(self): + """Turn the wifi led on.""" + if self._additional_supported_features & SUPPORT_SET_WIFI_LED == 0: + return + + await self._try_command( + "Turning the wifi led off failed.", + self._plug.set_wifi_led, False) + + async def async_set_power_price(self, price: int): + """Set the power price.""" + if self._additional_supported_features & SUPPORT_SET_POWER_PRICE == 0: + return + + await self._try_command( + "Setting the power price of the power strip failed.", + self._plug.set_power_price, price) + + +class XiaomiPowerStripSwitch(XiaomiPlugGenericSwitch): """Representation of a Xiaomi Power Strip.""" - def __init__(self, name, plug, model): + def __init__(self, name, plug, model, unique_id): """Initialize the plug switch.""" - XiaomiPlugGenericSwitch.__init__(self, name, plug, model) + XiaomiPlugGenericSwitch.__init__(self, name, plug, model, unique_id) - self._state_attrs = { - ATTR_TEMPERATURE: None, + if self._model == MODEL_POWER_STRIP_V2: + self._additional_supported_features = \ + ADDITIONAL_SUPPORT_FLAGS_POWER_STRIP_V2 + else: + self._additional_supported_features = \ + ADDITIONAL_SUPPORT_FLAGS_POWER_STRIP_V1 + + self._state_attrs.update({ ATTR_LOAD_POWER: None, - ATTR_MODEL: self._model, - } + }) - @asyncio.coroutine - def async_update(self): + if self._additional_supported_features & SUPPORT_SET_POWER_MODE == 1: + self._state_attrs[ATTR_POWER_MODE] = None + + if self._additional_supported_features & SUPPORT_SET_WIFI_LED == 1: + self._state_attrs[ATTR_WIFI_LED] = None + + if self._additional_supported_features & SUPPORT_SET_POWER_PRICE == 1: + self._state_attrs[ATTR_POWER_PRICE] = None + + async def async_update(self): """Fetch state from the device.""" from miio import DeviceException @@ -232,60 +363,84 @@ class XiaomiPowerStripSwitch(XiaomiPlugGenericSwitch, SwitchDevice): return try: - state = yield from self.hass.async_add_job(self._plug.status) + state = await self.hass.async_add_job(self._plug.status) _LOGGER.debug("Got new state: %s", state) + self._available = True self._state = state.is_on self._state_attrs.update({ ATTR_TEMPERATURE: state.temperature, - ATTR_LOAD_POWER: state.load_power + ATTR_LOAD_POWER: state.load_power, }) + if self._additional_supported_features & \ + SUPPORT_SET_POWER_MODE == 1 and state.mode: + self._state_attrs[ATTR_POWER_MODE] = state.mode.value + + if self._additional_supported_features & \ + SUPPORT_SET_WIFI_LED == 1 and state.wifi_led: + self._state_attrs[ATTR_WIFI_LED] = state.wifi_led + + if self._additional_supported_features & \ + SUPPORT_SET_POWER_PRICE == 1 and state.power_price: + self._state_attrs[ATTR_POWER_PRICE] = state.power_price + except DeviceException as ex: - self._state = None + self._available = False _LOGGER.error("Got exception while fetching the state: %s", ex) + async def async_set_power_mode(self, mode: str): + """Set the power mode.""" + if self._additional_supported_features & SUPPORT_SET_POWER_MODE == 0: + return -class ChuangMiPlugV1Switch(XiaomiPlugGenericSwitch, SwitchDevice): + from miio.powerstrip import PowerMode + + await self._try_command( + "Setting the power mode of the power strip failed.", + self._plug.set_power_mode, PowerMode(mode)) + + +class ChuangMiPlugV1Switch(XiaomiPlugGenericSwitch): """Representation of a Chuang Mi Plug V1.""" - def __init__(self, name, plug, model, channel_usb): + def __init__(self, name, plug, model, unique_id, channel_usb): """Initialize the plug switch.""" name = '{} USB'.format(name) if channel_usb else name - XiaomiPlugGenericSwitch.__init__(self, name, plug, model) + if unique_id is not None and channel_usb: + unique_id = "{}-{}".format(unique_id, 'usb') + + XiaomiPlugGenericSwitch.__init__(self, name, plug, model, unique_id) self._channel_usb = channel_usb - @asyncio.coroutine - def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): """Turn a channel on.""" if self._channel_usb: - result = yield from self._try_command( + result = await self._try_command( "Turning the plug on failed.", self._plug.usb_on) else: - result = yield from self._try_command( + result = await self._try_command( "Turning the plug on failed.", self._plug.on) if result: self._state = True self._skip_update = True - @asyncio.coroutine - def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs): """Turn a channel off.""" if self._channel_usb: - result = yield from self._try_command( + result = await self._try_command( "Turning the plug on failed.", self._plug.usb_off) else: - result = yield from self._try_command( + result = await self._try_command( "Turning the plug on failed.", self._plug.off) if result: self._state = False self._skip_update = True - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Fetch state from the device.""" from miio import DeviceException @@ -295,9 +450,10 @@ class ChuangMiPlugV1Switch(XiaomiPlugGenericSwitch, SwitchDevice): return try: - state = yield from self.hass.async_add_job(self._plug.status) + state = await self.hass.async_add_job(self._plug.status) _LOGGER.debug("Got new state: %s", state) + self._available = True if self._channel_usb: self._state = state.usb_power else: @@ -308,5 +464,5 @@ class ChuangMiPlugV1Switch(XiaomiPlugGenericSwitch, SwitchDevice): }) except DeviceException as ex: - self._state = None + self._available = False _LOGGER.error("Got exception while fetching the state: %s", ex) From fe7012549eec44725144476dc59d10fb52e390f6 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Fri, 16 Mar 2018 20:59:18 +0100 Subject: [PATCH 115/220] Xiaomi MiIO light: Philips Eyecare Smart Lamp 2 integration (#12883) * Xiaomi Philips Eyecare Smart Lamp 2 support added. * Blank lines removed. * Pylint errors fixed. * Abstract light introduced. * Smart night light mode renamed. * Use the conventional power on/off methods again. * Eyecare mode service added. * Eyecare mode attribute added. * Name of the ambient light entity fixed. * Reuse of the same local variable name within the same scope fixed. * Use Async / await syntax. * Missed method fixed. * Make hound happy. * Don't abuse the system property supported_features anymore. * Make hound happy. * Wrong hanging indentation fixed. Unnecessary parens after 'return' keyword fixed. * Refactoring. * Additional supported features bit mask removed as long as the differences of the supported devices are simple. * Support for Xiaomi Philips Zhirui Smart LED Bulb E14 Candle Lamp added. * Docstrings updated. Refactoring. * Unique id added. * Filter service calls. Dummy methods removed. * Device available handling improved. * super() used for calling the parent class * Self removed from super(). --- homeassistant/components/light/xiaomi_miio.py | 402 ++++++++++++++---- 1 file changed, 331 insertions(+), 71 deletions(-) diff --git a/homeassistant/components/light/xiaomi_miio.py b/homeassistant/components/light/xiaomi_miio.py index 77b02600f33..a21c86f49c0 100644 --- a/homeassistant/components/light/xiaomi_miio.py +++ b/homeassistant/components/light/xiaomi_miio.py @@ -37,7 +37,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ ['philips.light.sread1', 'philips.light.ceiling', 'philips.light.zyceiling', - 'philips.light.bulb']), + 'philips.light.bulb', + 'philips.light.candle2']), }) REQUIREMENTS = ['python-miio==0.3.8'] @@ -46,16 +47,27 @@ REQUIREMENTS = ['python-miio==0.3.8'] CCT_MIN = 1 CCT_MAX = 100 -DELAYED_TURN_OFF_MAX_DEVIATION = 4 +DELAYED_TURN_OFF_MAX_DEVIATION_SECONDS = 4 +DELAYED_TURN_OFF_MAX_DEVIATION_MINUTES = 1 SUCCESS = ['ok'] ATTR_MODEL = 'model' ATTR_SCENE = 'scene' ATTR_DELAYED_TURN_OFF = 'delayed_turn_off' ATTR_TIME_PERIOD = 'time_period' +ATTR_NIGHT_LIGHT_MODE = 'night_light_mode' +ATTR_AUTOMATIC_COLOR_TEMPERATURE = 'automatic_color_temperature' +ATTR_REMINDER = 'reminder' +ATTR_EYECARE_MODE = 'eyecare_mode' SERVICE_SET_SCENE = 'xiaomi_miio_set_scene' SERVICE_SET_DELAYED_TURN_OFF = 'xiaomi_miio_set_delayed_turn_off' +SERVICE_REMINDER_ON = 'xiaomi_miio_reminder_on' +SERVICE_REMINDER_OFF = 'xiaomi_miio_reminder_off' +SERVICE_NIGHT_LIGHT_MODE_ON = 'xiaomi_miio_night_light_mode_on' +SERVICE_NIGHT_LIGHT_MODE_OFF = 'xiaomi_miio_night_light_mode_off' +SERVICE_EYECARE_MODE_ON = 'xiaomi_miio_eyecare_mode_on' +SERVICE_EYECARE_MODE_OFF = 'xiaomi_miio_eyecare_mode_off' XIAOMI_MIIO_SERVICE_SCHEMA = vol.Schema({ vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, @@ -78,12 +90,18 @@ SERVICE_TO_METHOD = { SERVICE_SET_SCENE: { 'method': 'async_set_scene', 'schema': SERVICE_SCHEMA_SET_SCENE}, + SERVICE_REMINDER_ON: {'method': 'async_reminder_on'}, + SERVICE_REMINDER_OFF: {'method': 'async_reminder_off'}, + SERVICE_NIGHT_LIGHT_MODE_ON: {'method': 'async_night_light_mode_on'}, + SERVICE_NIGHT_LIGHT_MODE_OFF: {'method': 'async_night_light_mode_off'}, + SERVICE_EYECARE_MODE_ON: {'method': 'async_eyecare_mode_on'}, + SERVICE_EYECARE_MODE_OFF: {'method': 'async_eyecare_mode_off'}, } # pylint: disable=unused-argument -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_devices, discovery_info=None): +async def async_setup_platform(hass, config, async_add_devices, + discovery_info=None): """Set up the light from config.""" from miio import Device, DeviceException if DATA_KEY not in hass.data: @@ -96,11 +114,15 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): _LOGGER.info("Initializing with host %s (token %s...)", host, token[:5]) + devices = [] + unique_id = None + if model is None: try: miio_device = Device(host, token) device_info = miio_device.info() model = device_info.model + unique_id = "{}-{}".format(model, device_info.mac_address) _LOGGER.info("%s %s %s detected", model, device_info.firmware_version, @@ -111,27 +133,38 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): if model == 'philips.light.sread1': from miio import PhilipsEyecare light = PhilipsEyecare(host, token) - device = XiaomiPhilipsEyecareLamp(name, light, model) + primary_device = XiaomiPhilipsEyecareLamp( + name, light, model, unique_id) + devices.append(primary_device) + hass.data[DATA_KEY][host] = primary_device + + secondary_device = XiaomiPhilipsEyecareLampAmbientLight( + name, light, model, unique_id) + devices.append(secondary_device) + # The ambient light doesn't expose additional services. + # A hass.data[DATA_KEY] entry isn't needed. elif model in ['philips.light.ceiling', 'philips.light.zyceiling']: from miio import Ceil light = Ceil(host, token) - device = XiaomiPhilipsCeilingLamp(name, light, model) - elif model == 'philips.light.bulb': + device = XiaomiPhilipsCeilingLamp(name, light, model, unique_id) + devices.append(device) + hass.data[DATA_KEY][host] = device + elif model in ['philips.light.bulb', 'philips.light.candle2']: from miio import PhilipsBulb light = PhilipsBulb(host, token) - device = XiaomiPhilipsLightBall(name, light, model) + device = XiaomiPhilipsBulb(name, light, model, unique_id) + devices.append(device) + hass.data[DATA_KEY][host] = device else: _LOGGER.error( 'Unsupported device found! Please create an issue at ' - 'https://github.com/rytilahti/python-miio/issues ' + 'https://github.com/syssi/philipslight/issues ' 'and provide the following data: %s', model) return False - hass.data[DATA_KEY][host] = device - async_add_devices([device], update_before_add=True) + async_add_devices(devices, update_before_add=True) - @asyncio.coroutine - def async_service_handler(service): + async def async_service_handler(service): """Map services to methods on Xiaomi Philips Lights.""" method = SERVICE_TO_METHOD.get(service.service) params = {key: value for key, value in service.data.items() @@ -145,11 +178,13 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): update_tasks = [] for target_device in target_devices: - yield from getattr(target_device, method['method'])(**params) + if not hasattr(target_device, method['method']): + continue + await getattr(target_device, method['method'])(**params) update_tasks.append(target_device.async_update_ha_state(True)) if update_tasks: - yield from asyncio.wait(update_tasks, loop=hass.loop) + await asyncio.wait(update_tasks, loop=hass.loop) for xiaomi_miio_service in SERVICE_TO_METHOD: schema = SERVICE_TO_METHOD[xiaomi_miio_service].get( @@ -158,23 +193,22 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): DOMAIN, xiaomi_miio_service, async_service_handler, schema=schema) -class XiaomiPhilipsGenericLight(Light): - """Representation of a Xiaomi Philips Light.""" +class XiaomiPhilipsAbstractLight(Light): + """Representation of a Abstract Xiaomi Philips Light.""" - def __init__(self, name, light, model): + def __init__(self, name, light, model, unique_id): """Initialize the light device.""" self._name = name + self._light = light self._model = model + self._unique_id = unique_id self._brightness = None - self._color_temp = None - self._light = light + self._available = False self._state = None self._state_attrs = { ATTR_MODEL: self._model, - ATTR_SCENE: None, - ATTR_DELAYED_TURN_OFF: None, } @property @@ -182,6 +216,11 @@ class XiaomiPhilipsGenericLight(Light): """Poll the light.""" return True + @property + def unique_id(self): + """Return an unique ID.""" + return self._unique_id + @property def name(self): """Return the name of the device if any.""" @@ -190,7 +229,7 @@ class XiaomiPhilipsGenericLight(Light): @property def available(self): """Return true when state is known.""" - return self._state is not None + return self._available @property def device_state_attributes(self): @@ -212,12 +251,11 @@ class XiaomiPhilipsGenericLight(Light): """Return the supported features.""" return SUPPORT_BRIGHTNESS - @asyncio.coroutine - def _try_command(self, mask_error, func, *args, **kwargs): + async def _try_command(self, mask_error, func, *args, **kwargs): """Call a light command handling error messages.""" from miio import DeviceException try: - result = yield from self.hass.async_add_job( + result = await self.hass.async_add_job( partial(func, *args, **kwargs)) _LOGGER.debug("Response received from light: %s", result) @@ -225,10 +263,10 @@ class XiaomiPhilipsGenericLight(Light): return result == SUCCESS except DeviceException as exc: _LOGGER.error(mask_error, exc) + self._available = False return False - @asyncio.coroutine - def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): """Turn the light on.""" if ATTR_BRIGHTNESS in kwargs: brightness = kwargs[ATTR_BRIGHTNESS] @@ -238,30 +276,57 @@ class XiaomiPhilipsGenericLight(Light): "Setting brightness: %s %s%%", brightness, percent_brightness) - result = yield from self._try_command( + result = await self._try_command( "Setting brightness failed: %s", self._light.set_brightness, percent_brightness) if result: self._brightness = brightness else: - yield from self._try_command( + await self._try_command( "Turning the light on failed.", self._light.on) - @asyncio.coroutine - def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs): """Turn the light off.""" - yield from self._try_command( + await self._try_command( "Turning the light off failed.", self._light.off) - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Fetch state from the device.""" from miio import DeviceException try: - state = yield from self.hass.async_add_job(self._light.status) + state = await self.hass.async_add_job(self._light.status) _LOGGER.debug("Got new state: %s", state) + self._available = True + self._state = state.is_on + self._brightness = ceil((255 / 100.0) * state.brightness) + + except DeviceException as ex: + self._available = False + _LOGGER.error("Got exception while fetching the state: %s", ex) + + +class XiaomiPhilipsGenericLight(XiaomiPhilipsAbstractLight): + """Representation of a Generic Xiaomi Philips Light.""" + + def __init__(self, name, light, model, unique_id): + """Initialize the light device.""" + super().__init__(name, light, model, unique_id) + + self._state_attrs.update({ + ATTR_SCENE: None, + ATTR_DELAYED_TURN_OFF: None, + }) + + async def async_update(self): + """Fetch state from the device.""" + from miio import DeviceException + try: + state = await self.hass.async_add_job(self._light.status) + _LOGGER.debug("Got new state: %s", state) + + self._available = True self._state = state.is_on self._brightness = ceil((255 / 100.0) * state.brightness) @@ -276,45 +341,35 @@ class XiaomiPhilipsGenericLight(Light): }) except DeviceException as ex: - self._state = None + self._available = False _LOGGER.error("Got exception while fetching the state: %s", ex) - @asyncio.coroutine - def async_set_scene(self, scene: int = 1): + async def async_set_scene(self, scene: int = 1): """Set the fixed scene.""" - yield from self._try_command( + await self._try_command( "Setting a fixed scene failed.", self._light.set_scene, scene) - @asyncio.coroutine - def async_set_delayed_turn_off(self, time_period: timedelta): - """Set delay off. The unit is different per device.""" - yield from self._try_command( - "Setting the delay off failed.", + async def async_set_delayed_turn_off(self, time_period: timedelta): + """Set delayed turn off.""" + await self._try_command( + "Setting the turn off delay failed.", self._light.delay_off, time_period.total_seconds()) - @staticmethod - def translate(value, left_min, left_max, right_min, right_max): - """Map a value from left span to right span.""" - left_span = left_max - left_min - right_span = right_max - right_min - value_scaled = float(value - left_min) / float(left_span) - return int(right_min + (value_scaled * right_span)) - @staticmethod def delayed_turn_off_timestamp(countdown: int, current: datetime, previous: datetime): """Update the turn off timestamp only if necessary.""" - if countdown > 0: + if countdown is not None and countdown > 0: new = current.replace(microsecond=0) + \ timedelta(seconds=countdown) if previous is None: return new - lower = timedelta(seconds=-DELAYED_TURN_OFF_MAX_DEVIATION) - upper = timedelta(seconds=DELAYED_TURN_OFF_MAX_DEVIATION) + lower = timedelta(seconds=-DELAYED_TURN_OFF_MAX_DEVIATION_SECONDS) + upper = timedelta(seconds=DELAYED_TURN_OFF_MAX_DEVIATION_SECONDS) diff = previous - new if lower < diff < upper: return previous @@ -324,8 +379,14 @@ class XiaomiPhilipsGenericLight(Light): return None -class XiaomiPhilipsLightBall(XiaomiPhilipsGenericLight, Light): - """Representation of a Xiaomi Philips Light Ball.""" +class XiaomiPhilipsBulb(XiaomiPhilipsGenericLight): + """Representation of a Xiaomi Philips Bulb.""" + + def __init__(self, name, light, model, unique_id): + """Initialize the light device.""" + super().__init__(name, light, model, unique_id) + + self._color_temp = None @property def color_temp(self): @@ -347,8 +408,7 @@ class XiaomiPhilipsLightBall(XiaomiPhilipsGenericLight, Light): """Return the supported features.""" return SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP - @asyncio.coroutine - def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): """Turn the light on.""" if ATTR_COLOR_TEMP in kwargs: color_temp = kwargs[ATTR_COLOR_TEMP] @@ -367,7 +427,7 @@ class XiaomiPhilipsLightBall(XiaomiPhilipsGenericLight, Light): brightness, percent_brightness, color_temp, percent_color_temp) - result = yield from self._try_command( + result = await self._try_command( "Setting brightness and color temperature failed: " "%s bri, %s cct", self._light.set_brightness_and_color_temperature, @@ -383,7 +443,7 @@ class XiaomiPhilipsLightBall(XiaomiPhilipsGenericLight, Light): "%s mireds, %s%% cct", color_temp, percent_color_temp) - result = yield from self._try_command( + result = await self._try_command( "Setting color temperature failed: %s cct", self._light.set_color_temperature, percent_color_temp) @@ -398,7 +458,7 @@ class XiaomiPhilipsLightBall(XiaomiPhilipsGenericLight, Light): "Setting brightness: %s %s%%", brightness, percent_brightness) - result = yield from self._try_command( + result = await self._try_command( "Setting brightness failed: %s", self._light.set_brightness, percent_brightness) @@ -406,17 +466,17 @@ class XiaomiPhilipsLightBall(XiaomiPhilipsGenericLight, Light): self._brightness = brightness else: - yield from self._try_command( + await self._try_command( "Turning the light on failed.", self._light.on) - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Fetch state from the device.""" from miio import DeviceException try: - state = yield from self.hass.async_add_job(self._light.status) + state = await self.hass.async_add_job(self._light.status) _LOGGER.debug("Got new state: %s", state) + self._available = True self._state = state.is_on self._brightness = ceil((255 / 100.0) * state.brightness) self._color_temp = self.translate( @@ -435,13 +495,30 @@ class XiaomiPhilipsLightBall(XiaomiPhilipsGenericLight, Light): }) except DeviceException as ex: - self._state = None + self._available = False _LOGGER.error("Got exception while fetching the state: %s", ex) + @staticmethod + def translate(value, left_min, left_max, right_min, right_max): + """Map a value from left span to right span.""" + left_span = left_max - left_min + right_span = right_max - right_min + value_scaled = float(value - left_min) / float(left_span) + return int(right_min + (value_scaled * right_span)) -class XiaomiPhilipsCeilingLamp(XiaomiPhilipsLightBall, Light): + +class XiaomiPhilipsCeilingLamp(XiaomiPhilipsBulb): """Representation of a Xiaomi Philips Ceiling Lamp.""" + def __init__(self, name, light, model, unique_id): + """Initialize the light device.""" + super().__init__(name, light, model, unique_id) + + self._state_attrs.update({ + ATTR_NIGHT_LIGHT_MODE: None, + ATTR_AUTOMATIC_COLOR_TEMPERATURE: None, + }) + @property def min_mireds(self): """Return the coldest color_temp that this light supports.""" @@ -452,8 +529,191 @@ class XiaomiPhilipsCeilingLamp(XiaomiPhilipsLightBall, Light): """Return the warmest color_temp that this light supports.""" return 370 + async def async_update(self): + """Fetch state from the device.""" + from miio import DeviceException + try: + state = await self.hass.async_add_job(self._light.status) + _LOGGER.debug("Got new state: %s", state) -class XiaomiPhilipsEyecareLamp(XiaomiPhilipsGenericLight, Light): + self._available = True + self._state = state.is_on + self._brightness = ceil((255 / 100.0) * state.brightness) + self._color_temp = self.translate( + state.color_temperature, + CCT_MIN, CCT_MAX, + self.max_mireds, self.min_mireds) + + delayed_turn_off = self.delayed_turn_off_timestamp( + state.delay_off_countdown, + dt.utcnow(), + self._state_attrs[ATTR_DELAYED_TURN_OFF]) + + self._state_attrs.update({ + ATTR_SCENE: state.scene, + ATTR_DELAYED_TURN_OFF: delayed_turn_off, + ATTR_NIGHT_LIGHT_MODE: state.smart_night_light, + ATTR_AUTOMATIC_COLOR_TEMPERATURE: + state.automatic_color_temperature, + }) + + except DeviceException as ex: + self._available = False + _LOGGER.error("Got exception while fetching the state: %s", ex) + + +class XiaomiPhilipsEyecareLamp(XiaomiPhilipsGenericLight): """Representation of a Xiaomi Philips Eyecare Lamp 2.""" - pass + def __init__(self, name, light, model, unique_id): + """Initialize the light device.""" + super().__init__(name, light, model, unique_id) + + self._state_attrs.update({ + ATTR_REMINDER: None, + ATTR_NIGHT_LIGHT_MODE: None, + ATTR_EYECARE_MODE: None, + }) + + async def async_update(self): + """Fetch state from the device.""" + from miio import DeviceException + try: + state = await self.hass.async_add_job(self._light.status) + _LOGGER.debug("Got new state: %s", state) + + self._available = True + self._state = state.is_on + self._brightness = ceil((255 / 100.0) * state.brightness) + + delayed_turn_off = self.delayed_turn_off_timestamp( + state.delay_off_countdown, + dt.utcnow(), + self._state_attrs[ATTR_DELAYED_TURN_OFF]) + + self._state_attrs.update({ + ATTR_SCENE: state.scene, + ATTR_DELAYED_TURN_OFF: delayed_turn_off, + ATTR_REMINDER: state.reminder, + ATTR_NIGHT_LIGHT_MODE: state.smart_night_light, + ATTR_EYECARE_MODE: state.eyecare, + }) + + except DeviceException as ex: + self._available = False + _LOGGER.error("Got exception while fetching the state: %s", ex) + + async def async_set_delayed_turn_off(self, time_period: timedelta): + """Set delayed turn off.""" + await self._try_command( + "Setting the turn off delay failed.", + self._light.delay_off, round(time_period.total_seconds() / 60)) + + async def async_reminder_on(self): + """Enable the eye fatigue notification.""" + await self._try_command( + "Turning on the reminder failed.", + self._light.reminder_on) + + async def async_reminder_off(self): + """Disable the eye fatigue notification.""" + await self._try_command( + "Turning off the reminder failed.", + self._light.reminder_off) + + async def async_night_light_mode_on(self): + """Turn the smart night light mode on.""" + await self._try_command( + "Turning on the smart night light mode failed.", + self._light.smart_night_light_on) + + async def async_night_light_mode_off(self): + """Turn the smart night light mode off.""" + await self._try_command( + "Turning off the smart night light mode failed.", + self._light.smart_night_light_off) + + async def async_eyecare_mode_on(self): + """Turn the eyecare mode on.""" + await self._try_command( + "Turning on the eyecare mode failed.", + self._light.eyecare_on) + + async def async_eyecare_mode_off(self): + """Turn the eyecare mode off.""" + await self._try_command( + "Turning off the eyecare mode failed.", + self._light.eyecare_off) + + @staticmethod + def delayed_turn_off_timestamp(countdown: int, + current: datetime, + previous: datetime): + """Update the turn off timestamp only if necessary.""" + if countdown is not None and countdown > 0: + new = current.replace(second=0, microsecond=0) + \ + timedelta(minutes=countdown) + + if previous is None: + return new + + lower = timedelta(minutes=-DELAYED_TURN_OFF_MAX_DEVIATION_MINUTES) + upper = timedelta(minutes=DELAYED_TURN_OFF_MAX_DEVIATION_MINUTES) + diff = previous - new + if lower < diff < upper: + return previous + + return new + + return None + + +class XiaomiPhilipsEyecareLampAmbientLight(XiaomiPhilipsAbstractLight): + """Representation of a Xiaomi Philips Eyecare Lamp Ambient Light.""" + + def __init__(self, name, light, model, unique_id): + """Initialize the light device.""" + name = '{} Ambient Light'.format(name) + if unique_id is not None: + unique_id = "{}-{}".format(unique_id, 'ambient') + super().__init__(name, light, model, unique_id) + + async def async_turn_on(self, **kwargs): + """Turn the light on.""" + if ATTR_BRIGHTNESS in kwargs: + brightness = kwargs[ATTR_BRIGHTNESS] + percent_brightness = ceil(100 * brightness / 255.0) + + _LOGGER.debug( + "Setting brightness of the ambient light: %s %s%%", + brightness, percent_brightness) + + result = await self._try_command( + "Setting brightness of the ambient failed: %s", + self._light.set_ambient_brightness, percent_brightness) + + if result: + self._brightness = brightness + else: + await self._try_command( + "Turning the ambient light on failed.", self._light.ambient_on) + + async def async_turn_off(self, **kwargs): + """Turn the light off.""" + await self._try_command( + "Turning the ambient light off failed.", self._light.ambient_off) + + async def async_update(self): + """Fetch state from the device.""" + from miio import DeviceException + try: + state = await self.hass.async_add_job(self._light.status) + _LOGGER.debug("Got new state: %s", state) + + self._available = True + self._state = state.eyecare + self._brightness = ceil((255 / 100.0) * state.ambient_brightness) + + except DeviceException as ex: + self._available = False + _LOGGER.error("Got exception while fetching the state: %s", ex) From 88d2a6ab80f39a72dfbbe43cf9ee52f9c569ae5c Mon Sep 17 00:00:00 2001 From: Jens Timmerman Date: Fri, 16 Mar 2018 21:13:32 +0100 Subject: [PATCH 116/220] Fix guide link in CONTRIBUTING.md (#13272) --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a9c0c21d0d7..9ad922d7045 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,7 +4,7 @@ Everybody is invited and welcome to contribute to Home Assistant. There is a lot The process is straight-forward. - - Read [How to get faster PR reviews](https://github.com/kubernetes/community/blob/master/contributors/devel/pull-requests.md#best-practices-for-faster-reviews) by Kubernetes (but skip step 0) + - Read [How to get faster PR reviews](https://github.com/kubernetes/community/blob/master/contributors/guide/pull-requests.md#best-practices-for-faster-reviews) by Kubernetes (but skip step 0) - Fork the Home Assistant [git repository](https://github.com/home-assistant/home-assistant). - Write the code for your device, notification service, sensor, or IoT thing. - Ensure tests work. From ed6cd0ccfaa326e948e6ae9f2da4be6491a40c08 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Fri, 16 Mar 2018 21:15:23 +0100 Subject: [PATCH 117/220] Xiaomi MiIO Remote: Unique id added (#13266) * Unique id added. * Provide the exception as "ex" --- .../components/remote/xiaomi_miio.py | 27 ++++++++++++++----- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/remote/xiaomi_miio.py b/homeassistant/components/remote/xiaomi_miio.py index 30141eaf5e6..91f753391fc 100644 --- a/homeassistant/components/remote/xiaomi_miio.py +++ b/homeassistant/components/remote/xiaomi_miio.py @@ -18,6 +18,7 @@ from homeassistant.components.remote import ( from homeassistant.const import ( CONF_NAME, CONF_HOST, CONF_TOKEN, CONF_TIMEOUT, ATTR_ENTITY_ID, ATTR_HIDDEN, CONF_COMMAND) +from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv from homeassistant.util.dt import utcnow @@ -78,10 +79,16 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): # Check that we can communicate with device. try: - device.info() + device_info = device.info() + model = device_info.model + unique_id = "{}-{}".format(model, device_info.mac_address) + _LOGGER.info("%s %s %s detected", + model, + device_info.firmware_version, + device_info.hardware_version) except DeviceException as ex: - _LOGGER.error("Token not accepted by device : %s", ex) - return + _LOGGER.error("Device unavailable or token incorrect: %s", ex) + raise PlatformNotReady if DATA_KEY not in hass.data: hass.data[DATA_KEY] = {} @@ -93,9 +100,9 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): hidden = config.get(ATTR_HIDDEN) - xiaomi_miio_remote = XiaomiMiioRemote( - friendly_name, device, slot, timeout, - hidden, config.get(CONF_COMMANDS)) + xiaomi_miio_remote = XiaomiMiioRemote(friendly_name, device, unique_id, + slot, timeout, hidden, + config.get(CONF_COMMANDS)) hass.data[DATA_KEY][host] = xiaomi_miio_remote @@ -158,17 +165,23 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): class XiaomiMiioRemote(RemoteDevice): """Representation of a Xiaomi Miio Remote device.""" - def __init__(self, friendly_name, device, + def __init__(self, friendly_name, device, unique_id, slot, timeout, hidden, commands): """Initialize the remote.""" self._name = friendly_name self._device = device + self._unique_id = unique_id self._is_hidden = hidden self._slot = slot self._timeout = timeout self._state = False self._commands = commands + @property + def unique_id(self): + """Return an unique ID.""" + return self._unique_id + @property def name(self): """Return the name of the remote.""" From d04ba3f86d9aacda0bc1c890068c6ff50463cb17 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Fri, 16 Mar 2018 22:13:04 +0100 Subject: [PATCH 118/220] Xiaomi MiIO Sensor: Xiaomi Air Quality Monitor (PM2.5) integration (#13264) * Xiaomi MiIO Sensor: Xiaomi Air Quality Monitor (PM2.5) integration. * Missing newline added. * Use a unique data key per domain. * turn_{on,off} service moved to __init__.py. * All sensors group added. * Sensor is a ToggleEntity now. * is_on property added. * Use Async / await syntax. * Make hound happy. * Unique id added. * Turn on/off service removed from abstract sensor. * Turn on/off methods removed. Device unavailable handling improved. * Unused import removed. * Sensor migrated back to an entity. * Rebased and requirements updated. --- .../components/sensor/xiaomi_miio.py | 168 ++++++++++++++++++ requirements_all.txt | 1 + 2 files changed, 169 insertions(+) create mode 100644 homeassistant/components/sensor/xiaomi_miio.py diff --git a/homeassistant/components/sensor/xiaomi_miio.py b/homeassistant/components/sensor/xiaomi_miio.py new file mode 100644 index 00000000000..af7534d9112 --- /dev/null +++ b/homeassistant/components/sensor/xiaomi_miio.py @@ -0,0 +1,168 @@ +""" +Support for Xiaomi Mi Air Quality Monitor (PM2.5). + +For more details about this platform, please refer to the documentation +https://home-assistant.io/components/sensor.xiaomi_miio/ +""" +from functools import partial +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, CONF_HOST, CONF_TOKEN) +from homeassistant.exceptions import PlatformNotReady + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_NAME = 'Xiaomi Miio Sensor' +DATA_KEY = 'sensor.xiaomi_miio' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_TOKEN): vol.All(cv.string, vol.Length(min=32, max=32)), + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, +}) + +REQUIREMENTS = ['python-miio==0.3.8'] + +ATTR_POWER = 'power' +ATTR_CHARGING = 'charging' +ATTR_BATTERY_LEVEL = 'battery_level' +ATTR_TIME_STATE = 'time_state' +ATTR_MODEL = 'model' + +SUCCESS = ['ok'] + + +# pylint: disable=unused-argument +async def async_setup_platform(hass, config, async_add_devices, + discovery_info=None): + """Set up the sensor from config.""" + from miio import AirQualityMonitor, DeviceException + if DATA_KEY not in hass.data: + hass.data[DATA_KEY] = {} + + host = config.get(CONF_HOST) + name = config.get(CONF_NAME) + token = config.get(CONF_TOKEN) + + _LOGGER.info("Initializing with host %s (token %s...)", host, token[:5]) + + try: + air_quality_monitor = AirQualityMonitor(host, token) + device_info = air_quality_monitor.info() + model = device_info.model + unique_id = "{}-{}".format(model, device_info.mac_address) + _LOGGER.info("%s %s %s detected", + model, + device_info.firmware_version, + device_info.hardware_version) + device = XiaomiAirQualityMonitor( + name, air_quality_monitor, model, unique_id) + except DeviceException: + raise PlatformNotReady + + hass.data[DATA_KEY][host] = device + async_add_devices([device], update_before_add=True) + + +class XiaomiAirQualityMonitor(Entity): + """Representation of a Xiaomi Air Quality Monitor.""" + + def __init__(self, name, device, model, unique_id): + """Initialize the entity.""" + self._name = name + self._device = device + self._model = model + self._unique_id = unique_id + + self._icon = 'mdi:cloud' + self._unit_of_measurement = 'AQI' + self._available = None + self._state = None + self._state_attrs = { + ATTR_POWER: None, + ATTR_BATTERY_LEVEL: None, + ATTR_CHARGING: None, + ATTR_TIME_STATE: None, + ATTR_MODEL: self._model, + } + + @property + def should_poll(self): + """Poll the miio device.""" + return True + + @property + def unique_id(self): + """Return an unique ID.""" + return self._unique_id + + @property + def name(self): + """Return the name of this entity, if any.""" + return self._name + + @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): + """Return the icon to use for device if any.""" + return self._icon + + @property + def available(self): + """Return true when state is known.""" + return self._available + + @property + def state(self): + """Return the state of the device.""" + return self._state + + @property + def device_state_attributes(self): + """Return the state attributes of the device.""" + return self._state_attrs + + async def _try_command(self, mask_error, func, *args, **kwargs): + """Call a device command handling error messages.""" + from miio import DeviceException + try: + result = await self.hass.async_add_job( + partial(func, *args, **kwargs)) + + _LOGGER.debug("Response received from miio device: %s", result) + + return result == SUCCESS + except DeviceException as exc: + _LOGGER.error(mask_error, exc) + self._available = False + return False + + async def async_update(self): + """Fetch state from the miio device.""" + from miio import DeviceException + + try: + state = await self.hass.async_add_job(self._device.status) + _LOGGER.debug("Got new state: %s", state) + + self._available = True + self._state = state.aqi + self._state_attrs.update({ + ATTR_POWER: state.power, + ATTR_CHARGING: state.usb_power, + ATTR_BATTERY_LEVEL: state.battery, + ATTR_TIME_STATE: state.time_state, + }) + + except DeviceException as ex: + self._available = False + _LOGGER.error("Got exception while fetching the state: %s", ex) diff --git a/requirements_all.txt b/requirements_all.txt index b30b77307c1..732f054f7b3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -942,6 +942,7 @@ python-juicenet==0.0.5 # homeassistant.components.fan.xiaomi_miio # homeassistant.components.light.xiaomi_miio # homeassistant.components.remote.xiaomi_miio +# homeassistant.components.sensor.xiaomi_miio # homeassistant.components.switch.xiaomi_miio # homeassistant.components.vacuum.xiaomi_miio python-miio==0.3.8 From d78e75db66c43683a0711aa501bbb42d14164715 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 16 Mar 2018 15:39:26 -0700 Subject: [PATCH 119/220] Bump frontend to 20180316.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 153d1f6564e..eccc47e05c7 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -24,7 +24,7 @@ from homeassistant.core import callback from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass -REQUIREMENTS = ['home-assistant-frontend==20180315.0'] +REQUIREMENTS = ['home-assistant-frontend==20180316.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index 732f054f7b3..f25200ba49e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -356,7 +356,7 @@ hipnotify==1.0.8 holidays==0.9.4 # homeassistant.components.frontend -home-assistant-frontend==20180315.0 +home-assistant-frontend==20180316.0 # homeassistant.components.camera.onvif http://github.com/tgaugry/suds-passworddigest-py3/archive/86fc50e39b4d2b8997481967d6a7fe1c57118999.zip#suds-passworddigest-py3==0.1.2a diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 69b56eabc5e..8b3cc8d207a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.1 holidays==0.9.4 # homeassistant.components.frontend -home-assistant-frontend==20180315.0 +home-assistant-frontend==20180316.0 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From 5a9013cda58a3a99e1f1e9fa73e18c5895a4f56c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 16 Mar 2018 20:27:05 -0700 Subject: [PATCH 120/220] Refactor Hue: phue -> aiohue (#13043) * phue -> aiohue * Clean up * Fix config * Address comments * Typo * Fix rebase error * Mark light as unavailable when bridge is disconnected * Tests * Make Throttle work with double delay and async * Rework update logic * Don't resolve host to IP * Clarify comment * No longer do unnecessary updates * Add more doc * Another comment update * Wrap up tests * Lint * Fix tests * PyLint does not like mix 'n match async and coroutine * Lint * Update aiohue to 1.2 * Lint * Fix await MagicMock --- homeassistant/components/discovery.py | 16 +- homeassistant/components/hue/__init__.py | 261 ++--- homeassistant/components/light/hue.py | 367 ++++--- homeassistant/components/mqtt/discovery.py | 14 +- homeassistant/components/zwave/__init__.py | 20 +- homeassistant/core.py | 2 +- homeassistant/helpers/discovery.py | 18 +- homeassistant/util/__init__.py | 20 +- requirements_all.txt | 5 +- requirements_test_all.txt | 2 +- tests/components/hue/__init__.py | 1 + tests/components/hue/conftest.py | 17 + tests/components/hue/test_bridge.py | 98 ++ tests/components/hue/test_config_flow.py | 184 ++++ tests/components/hue/test_setup.py | 74 ++ tests/components/light/test_hue.py | 1051 ++++++++++---------- tests/components/test_hue.py | 588 ----------- tests/components/zwave/test_init.py | 14 +- tests/helpers/test_discovery.py | 14 +- tests/util/test_init.py | 8 + 20 files changed, 1289 insertions(+), 1485 deletions(-) create mode 100644 tests/components/hue/__init__.py create mode 100644 tests/components/hue/conftest.py create mode 100644 tests/components/hue/test_bridge.py create mode 100644 tests/components/hue/test_config_flow.py create mode 100644 tests/components/hue/test_setup.py delete mode 100644 tests/components/test_hue.py diff --git a/homeassistant/components/discovery.py b/homeassistant/components/discovery.py index 21a339602dd..6ab7f42558b 100644 --- a/homeassistant/components/discovery.py +++ b/homeassistant/components/discovery.py @@ -6,7 +6,6 @@ Will emit EVENT_PLATFORM_DISCOVERED whenever a new service has been discovered. Knows which components handle certain types, will make sure they are loaded before the EVENT_PLATFORM_DISCOVERED is fired. """ -import asyncio import json from datetime import timedelta import logging @@ -84,8 +83,7 @@ CONFIG_SCHEMA = vol.Schema({ }, extra=vol.ALLOW_EXTRA) -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Start a discovery service.""" from netdisco.discovery import NetworkDiscovery @@ -99,8 +97,7 @@ def async_setup(hass, config): # Platforms ignore by config ignored_platforms = config[DOMAIN][CONF_IGNORE] - @asyncio.coroutine - def new_service_found(service, info): + async def new_service_found(service, info): """Handle a new service if one is found.""" if service in ignored_platforms: logger.info("Ignoring service: %s %s", service, info) @@ -124,15 +121,14 @@ def async_setup(hass, config): component, platform = comp_plat if platform is None: - yield from async_discover(hass, service, info, component, config) + await async_discover(hass, service, info, component, config) else: - yield from async_load_platform( + await async_load_platform( hass, component, platform, info, config) - @asyncio.coroutine - def scan_devices(now): + async def scan_devices(now): """Scan for devices.""" - results = yield from hass.async_add_job(_discover, netdisco) + results = await hass.async_add_job(_discover, netdisco) for result in results: hass.async_add_job(new_service_found(*result)) diff --git a/homeassistant/components/hue/__init__.py b/homeassistant/components/hue/__init__.py index f15052fbd67..2fb55f8f6e0 100644 --- a/homeassistant/components/hue/__init__.py +++ b/homeassistant/components/hue/__init__.py @@ -6,22 +6,22 @@ https://home-assistant.io/components/hue/ """ import asyncio import json -from functools import partial +import ipaddress import logging import os -import socket import async_timeout -import requests import voluptuous as vol +from homeassistant.core import callback from homeassistant.components.discovery import SERVICE_HUE from homeassistant.const import CONF_FILENAME, CONF_HOST import homeassistant.helpers.config_validation as cv from homeassistant.helpers import discovery, aiohttp_client from homeassistant import config_entries +from homeassistant.util.json import save_json -REQUIREMENTS = ['phue==1.0', 'aiohue==0.3.0'] +REQUIREMENTS = ['aiohue==1.2.0'] _LOGGER = logging.getLogger(__name__) @@ -36,26 +36,23 @@ DEFAULT_ALLOW_UNREACHABLE = False PHUE_CONFIG_FILE = 'phue.conf' -CONF_ALLOW_IN_EMULATED_HUE = "allow_in_emulated_hue" -DEFAULT_ALLOW_IN_EMULATED_HUE = True - CONF_ALLOW_HUE_GROUPS = "allow_hue_groups" DEFAULT_ALLOW_HUE_GROUPS = True -BRIDGE_CONFIG_SCHEMA = vol.Schema([{ - vol.Optional(CONF_HOST): cv.string, +BRIDGE_CONFIG_SCHEMA = vol.Schema({ + # Validate as IP address and then convert back to a string. + vol.Required(CONF_HOST): vol.All(ipaddress.ip_address, cv.string), vol.Optional(CONF_FILENAME, default=PHUE_CONFIG_FILE): cv.string, vol.Optional(CONF_ALLOW_UNREACHABLE, default=DEFAULT_ALLOW_UNREACHABLE): cv.boolean, - vol.Optional(CONF_ALLOW_IN_EMULATED_HUE, - default=DEFAULT_ALLOW_IN_EMULATED_HUE): cv.boolean, vol.Optional(CONF_ALLOW_HUE_GROUPS, default=DEFAULT_ALLOW_HUE_GROUPS): cv.boolean, -}]) +}) CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ - vol.Optional(CONF_BRIDGES): BRIDGE_CONFIG_SCHEMA, + vol.Optional(CONF_BRIDGES): + vol.All(cv.ensure_list, [BRIDGE_CONFIG_SCHEMA]), }), }, extra=vol.ALLOW_EXTRA) @@ -73,7 +70,7 @@ Press the button on the bridge to register Philips Hue with Home Assistant. """ -def setup(hass, config): +async def async_setup(hass, config): """Set up the Hue platform.""" conf = config.get(DOMAIN) if conf is None: @@ -82,135 +79,130 @@ def setup(hass, config): if DOMAIN not in hass.data: hass.data[DOMAIN] = {} - discovery.listen( - hass, - SERVICE_HUE, - lambda service, discovery_info: - bridge_discovered(hass, service, discovery_info)) + async def async_bridge_discovered(service, discovery_info): + """Dispatcher for Hue discovery events.""" + # Ignore emulated hue + if "HASS Bridge" in discovery_info.get('name', ''): + return + + await async_setup_bridge( + hass, discovery_info['host'], + 'phue-{}.conf'.format(discovery_info['serial'])) + + discovery.async_listen(hass, SERVICE_HUE, async_bridge_discovered) # User has configured bridges if CONF_BRIDGES in conf: bridges = conf[CONF_BRIDGES] + # Component is part of config but no bridges specified, discover. elif DOMAIN in config: # discover from nupnp - hosts = requests.get(API_NUPNP).json() - bridges = [{ + websession = aiohttp_client.async_get_clientsession(hass) + + async with websession.get(API_NUPNP) as req: + hosts = await req.json() + + # Run through config schema to populate defaults + bridges = [BRIDGE_CONFIG_SCHEMA({ CONF_HOST: entry['internalipaddress'], CONF_FILENAME: '.hue_{}.conf'.format(entry['id']), - } for entry in hosts] + }) for entry in hosts] + else: # Component not specified in config, we're loaded via discovery bridges = [] - for bridge in bridges: - filename = bridge.get(CONF_FILENAME) - allow_unreachable = bridge.get(CONF_ALLOW_UNREACHABLE) - allow_in_emulated_hue = bridge.get(CONF_ALLOW_IN_EMULATED_HUE) - allow_hue_groups = bridge.get(CONF_ALLOW_HUE_GROUPS) + if not bridges: + return True - host = bridge.get(CONF_HOST) - - if host is None: - host = _find_host_from_config(hass, filename) - - if host is None: - _LOGGER.error("No host found in configuration") - return False - - setup_bridge(host, hass, filename, allow_unreachable, - allow_in_emulated_hue, allow_hue_groups) + await asyncio.wait([ + async_setup_bridge( + hass, bridge[CONF_HOST], bridge[CONF_FILENAME], + bridge[CONF_ALLOW_UNREACHABLE], bridge[CONF_ALLOW_HUE_GROUPS] + ) for bridge in bridges + ]) return True -def bridge_discovered(hass, service, discovery_info): - """Dispatcher for Hue discovery events.""" - if "HASS Bridge" in discovery_info.get('name', ''): - return - - host = discovery_info.get('host') - serial = discovery_info.get('serial') - - filename = 'phue-{}.conf'.format(serial) - setup_bridge(host, hass, filename) - - -def setup_bridge(host, hass, filename=None, allow_unreachable=False, - allow_in_emulated_hue=True, allow_hue_groups=True, - username=None): +async def async_setup_bridge( + hass, host, filename=None, + allow_unreachable=DEFAULT_ALLOW_UNREACHABLE, + allow_hue_groups=DEFAULT_ALLOW_HUE_GROUPS, + username=None): """Set up a given Hue bridge.""" + assert filename or username, 'Need to pass at least a username or filename' + # Only register a device once - if socket.gethostbyname(host) in hass.data[DOMAIN]: + if host in hass.data[DOMAIN]: return + if username is None: + username = await hass.async_add_job( + _find_username_from_config, hass, filename) + bridge = HueBridge(host, hass, filename, username, allow_unreachable, - allow_in_emulated_hue, allow_hue_groups) - bridge.setup() + allow_hue_groups) + await bridge.async_setup() + hass.data[DOMAIN][host] = bridge -def _find_host_from_config(hass, filename=PHUE_CONFIG_FILE): - """Attempt to detect host based on existing configuration.""" +def _find_username_from_config(hass, filename): + """Load username from config.""" path = hass.config.path(filename) if not os.path.isfile(path): return None - try: - with open(path) as inp: - return next(iter(json.load(inp).keys())) - except (ValueError, AttributeError, StopIteration): - # ValueError if can't parse as JSON - # AttributeError if JSON value is not a dict - # StopIteration if no keys - return None + with open(path) as inp: + return list(json.load(inp).values())[0]['username'] class HueBridge(object): """Manages a single Hue bridge.""" - def __init__(self, host, hass, filename, username, allow_unreachable=False, - allow_in_emulated_hue=True, allow_hue_groups=True): + def __init__(self, host, hass, filename, username, + allow_unreachable=False, allow_groups=True): """Initialize the system.""" self.host = host - self.bridge_id = socket.gethostbyname(host) self.hass = hass self.filename = filename self.username = username self.allow_unreachable = allow_unreachable - self.allow_in_emulated_hue = allow_in_emulated_hue - self.allow_hue_groups = allow_hue_groups - + self.allow_groups = allow_groups self.available = True - self.bridge = None - self.lights = {} - self.lightgroups = {} - - self.configured = False self.config_request_id = None + self.api = None - hass.data[DOMAIN][self.bridge_id] = self - - def setup(self): + async def async_setup(self): """Set up a phue bridge based on host parameter.""" - import phue + import aiohue + + api = aiohue.Bridge( + self.host, + username=self.username, + websession=aiohttp_client.async_get_clientsession(self.hass) + ) try: - kwargs = {} - if self.username is not None: - kwargs['username'] = self.username - if self.filename is not None: - kwargs['config_file_path'] = \ - self.hass.config.path(self.filename) - self.bridge = phue.Bridge(self.host, **kwargs) - except OSError: # Wrong host was given + with async_timeout.timeout(5): + # Initialize bridge and validate our username + if not self.username: + await api.create_user('home-assistant') + await api.initialize() + except (aiohue.LinkButtonNotPressed, aiohue.Unauthorized): + _LOGGER.warning("Connected to Hue at %s but not registered.", + self.host) + self.async_request_configuration() + return + except (asyncio.TimeoutError, aiohue.RequestError): _LOGGER.error("Error connecting to the Hue bridge at %s", self.host) return - except phue.PhueRegistrationException: - _LOGGER.warning("Connected to Hue at %s but not registered.", - self.host) - self.request_configuration() + except aiohue.AiohueException: + _LOGGER.exception('Unknown Hue linking error occurred') + self.async_request_configuration() return except Exception: # pylint: disable=broad-except _LOGGER.exception("Unknown error connecting with Hue bridge at %s", @@ -221,57 +213,77 @@ class HueBridge(object): if self.config_request_id: request_id = self.config_request_id self.config_request_id = None - configurator = self.hass.components.configurator - configurator.request_done(request_id) + self.hass.components.configurator.async_request_done(request_id) - self.configured = True + self.username = api.username - discovery.load_platform( + # Save config file + await self.hass.async_add_job( + save_json, self.hass.config.path(self.filename), + {self.host: {'username': api.username}}) + + self.api = api + + self.hass.async_add_job(discovery.async_load_platform( self.hass, 'light', DOMAIN, - {'bridge_id': self.bridge_id}) + {'host': self.host})) - # create a service for calling run_scene directly on the bridge, - # used to simplify automation rules. - def hue_activate_scene(call): - """Service to call directly into bridge to set scenes.""" - group_name = call.data[ATTR_GROUP_NAME] - scene_name = call.data[ATTR_SCENE_NAME] - self.bridge.run_scene(group_name, scene_name) - - self.hass.services.register( - DOMAIN, SERVICE_HUE_SCENE, hue_activate_scene, + self.hass.services.async_register( + DOMAIN, SERVICE_HUE_SCENE, self.hue_activate_scene, schema=SCENE_SCHEMA) - def request_configuration(self): + @callback + def async_request_configuration(self): """Request configuration steps from the user.""" configurator = self.hass.components.configurator # We got an error if this method is called while we are configuring if self.config_request_id: - configurator.notify_errors( + configurator.async_notify_errors( self.config_request_id, "Failed to register, please try again.") return - self.config_request_id = configurator.request_config( - "Philips Hue", - lambda data: self.setup(), + async def config_callback(data): + """Callback for configurator data.""" + await self.async_setup() + + self.config_request_id = configurator.async_request_config( + "Philips Hue", config_callback, description=CONFIG_INSTRUCTIONS, entity_picture="/static/images/logo_philips_hue.png", submit_caption="I have pressed the button" ) - def get_api(self): - """Return the full api dictionary from phue.""" - return self.bridge.get_api() + async def hue_activate_scene(self, call, updated=False): + """Service to call directly into bridge to set scenes.""" + group_name = call.data[ATTR_GROUP_NAME] + scene_name = call.data[ATTR_SCENE_NAME] - def set_light(self, light_id, command): - """Adjust properties of one or more lights. See phue for details.""" - return self.bridge.set_light(light_id, command) + group = next( + (group for group in self.api.groups.values() + if group.name == group_name), None) - def set_group(self, light_id, command): - """Change light settings for a group. See phue for detail.""" - return self.bridge.set_group(light_id, command) + scene_id = next( + (scene.id for scene in self.api.scenes.values() + if scene.name == scene_name), None) + + # If we can't find it, fetch latest info. + if not updated and (group is None or scene_id is None): + await self.api.groups.update() + await self.api.scenes.update() + await self.hue_activate_scene(call, updated=True) + return + + if group is None: + _LOGGER.warning('Unable to find group %s', group_name) + return + + if scene_id is None: + _LOGGER.warning('Unable to find scene %s', scene_name) + return + + await group.set_action(scene=scene_id) @config_entries.HANDLERS.register(DOMAIN) @@ -374,7 +386,6 @@ class HueFlowHandler(config_entries.ConfigFlowHandler): async def async_setup_entry(hass, entry): """Set up a bridge for a config entry.""" - await hass.async_add_job(partial( - setup_bridge, entry.data['host'], hass, - username=entry.data['username'])) + await async_setup_bridge(hass, entry.data['host'], + username=entry.data['username']) return True diff --git a/homeassistant/components/light/hue.py b/homeassistant/components/light/hue.py index 661b7c2b3a1..c45d9c5c44e 100644 --- a/homeassistant/components/light/hue.py +++ b/homeassistant/components/light/hue.py @@ -8,31 +8,23 @@ import asyncio from datetime import timedelta import logging import random -import re -import socket -import voluptuous as vol +import async_timeout import homeassistant.components.hue as hue from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_FLASH, ATTR_RGB_COLOR, ATTR_TRANSITION, ATTR_XY_COLOR, EFFECT_COLORLOOP, EFFECT_RANDOM, - FLASH_LONG, FLASH_SHORT, PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, + FLASH_LONG, FLASH_SHORT, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, SUPPORT_FLASH, SUPPORT_RGB_COLOR, SUPPORT_TRANSITION, SUPPORT_XY_COLOR, Light) -from homeassistant.const import CONF_FILENAME, CONF_HOST, DEVICE_DEFAULT_NAME -import homeassistant.helpers.config_validation as cv -import homeassistant.util as util -from homeassistant.util import yaml import homeassistant.util.color as color_util DEPENDENCIES = ['hue'] +SCAN_INTERVAL = timedelta(seconds=5) _LOGGER = logging.getLogger(__name__) -MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) -MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(milliseconds=100) - SUPPORT_HUE_ON_OFF = (SUPPORT_FLASH | SUPPORT_TRANSITION) SUPPORT_HUE_DIMMABLE = (SUPPORT_HUE_ON_OFF | SUPPORT_BRIGHTNESS) SUPPORT_HUE_COLOR_TEMP = (SUPPORT_HUE_DIMMABLE | SUPPORT_COLOR_TEMP) @@ -48,244 +40,232 @@ SUPPORT_HUE = { 'Color temperature light': SUPPORT_HUE_COLOR_TEMP } -ATTR_EMULATED_HUE_HIDDEN = 'emulated_hue_hidden' ATTR_IS_HUE_GROUP = 'is_hue_group' - -# Legacy configuration, will be removed in 0.60 -CONF_ALLOW_UNREACHABLE = 'allow_unreachable' -DEFAULT_ALLOW_UNREACHABLE = False -CONF_ALLOW_IN_EMULATED_HUE = 'allow_in_emulated_hue' -DEFAULT_ALLOW_IN_EMULATED_HUE = True -CONF_ALLOW_HUE_GROUPS = 'allow_hue_groups' -DEFAULT_ALLOW_HUE_GROUPS = True - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_HOST): cv.string, - vol.Optional(CONF_ALLOW_UNREACHABLE): cv.boolean, - vol.Optional(CONF_FILENAME): cv.string, - vol.Optional(CONF_ALLOW_IN_EMULATED_HUE): cv.boolean, - vol.Optional(CONF_ALLOW_HUE_GROUPS, - default=DEFAULT_ALLOW_HUE_GROUPS): cv.boolean, -}) - -MIGRATION_ID = 'light_hue_config_migration' -MIGRATION_TITLE = 'Philips Hue Configuration Migration' -MIGRATION_INSTRUCTIONS = """ -Configuration for the Philips Hue component has changed; action required. - -You have configured at least one bridge: - - hue: -{config} - -This configuration is deprecated, please check the -[Hue component](https://home-assistant.io/components/hue/) page for more -information. -""" - -SIGNAL_CALLBACK = 'hue_light_callback_{}_{}' +# Minimum Hue Bridge API version to support groups +# 1.4.0 introduced extended group info +# 1.12 introduced the state object for groups +# 1.13 introduced "any_on" to group state objects +GROUP_MIN_API_VERSION = (1, 13, 0) -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 the Hue lights.""" - if discovery_info is None or 'bridge_id' not in discovery_info: + if discovery_info is None: return - if config is not None and config: - # Legacy configuration, will be removed in 0.60 - config_str = yaml.dump([config]) - # Indent so it renders in a fixed-width font - config_str = re.sub('(?m)^', ' ', config_str) - hass.components.persistent_notification.async_create( - MIGRATION_INSTRUCTIONS.format(config=config_str), - title=MIGRATION_TITLE, - notification_id=MIGRATION_ID) + bridge = hass.data[hue.DOMAIN][discovery_info['host']] + cur_lights = {} + cur_groups = {} - bridge_id = discovery_info['bridge_id'] - bridge = hass.data[hue.DOMAIN][bridge_id] - unthrottled_update_lights(hass, bridge, add_devices) + api_version = tuple( + int(v) for v in bridge.api.config.apiversion.split('.')) + + allow_groups = bridge.allow_groups + if allow_groups and api_version < GROUP_MIN_API_VERSION: + _LOGGER.warning('Please update your Hue bridge to support groups') + allow_groups = False + + # Hue updates all lights via a single API call. + # + # If we call a service to update 2 lights, we only want the API to be + # called once. + # + # The throttle decorator will return right away if a call is currently + # in progress. This means that if we are updating 2 lights, the first one + # is in the update method, the second one will skip it and assume the + # update went through and updates it's data, not good! + # + # The current mechanism will make sure that all lights will wait till + # the update call is done before writing their data to the state machine. + # + # An alternative approach would be to disable automatic polling by Home + # Assistant and take control ourselves. This works great for polling as now + # we trigger from 1 time update an update to all entities. However it gets + # tricky from inside async_turn_on and async_turn_off. + # + # If automatic polling is enabled, Home Assistant will call the entity + # update method after it is done calling all the services. This means that + # when we update, we know all commands have been processed. If we trigger + # the update from inside async_turn_on, the update will not capture the + # changes to the second entity until the next polling update because the + # throttle decorator will prevent the call. + + progress = None + light_progress = set() + group_progress = set() + + async def request_update(is_group, object_id): + """Request an update. + + We will only make 1 request to the server for updating at a time. If a + request is in progress, we will join the request that is in progress. + + This approach is possible because should_poll=True. That means that + Home Assistant will ask lights for updates during a polling cycle or + after it has called a service. + + We keep track of the lights that are waiting for the request to finish. + When new data comes in, we'll trigger an update for all non-waiting + lights. This covers the case where a service is called to enable 2 + lights but in the meanwhile some other light has changed too. + """ + nonlocal progress + + progress_set = group_progress if is_group else light_progress + progress_set.add(object_id) + + if progress is not None: + return await progress + + progress = asyncio.ensure_future(update_bridge()) + result = await progress + progress = None + light_progress.clear() + group_progress.clear() + return result + + async def update_bridge(): + """Update the values of the bridge. + + Will update lights and, if enabled, groups from the bridge. + """ + tasks = [] + tasks.append(async_update_items( + hass, bridge, async_add_devices, request_update, + False, cur_lights, light_progress + )) + + if allow_groups: + tasks.append(async_update_items( + hass, bridge, async_add_devices, request_update, + True, cur_groups, group_progress + )) + + await asyncio.wait(tasks) + + await update_bridge() -@util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS) -def update_lights(hass, bridge, add_devices): - """Update the Hue light objects with latest info from the bridge.""" - return unthrottled_update_lights(hass, bridge, add_devices) +async def async_update_items(hass, bridge, async_add_devices, + request_bridge_update, is_group, current, + progress_waiting): + """Update either groups or lights from the bridge.""" + import aiohue - -def unthrottled_update_lights(hass, bridge, add_devices): - """Update the lights (Internal version of update_lights).""" - import phue - - if not bridge.configured: - return + if is_group: + api = bridge.api.groups + else: + api = bridge.api.lights try: - api = bridge.get_api() - except phue.PhueRequestTimeout: - _LOGGER.warning("Timeout trying to reach the bridge") - bridge.available = False - return - except ConnectionRefusedError: - _LOGGER.error("The bridge refused the connection") - bridge.available = False - return - except socket.error: - # socket.error when we cannot reach Hue - _LOGGER.exception("Cannot reach the bridge") + with async_timeout.timeout(4): + await api.update() + except (asyncio.TimeoutError, aiohue.AiohueException): + if not bridge.available: + return + + _LOGGER.error('Unable to reach bridge %s', bridge.host) bridge.available = False + + for light_id, light in current.items(): + if light_id not in progress_waiting: + light.async_schedule_update_ha_state() + return - bridge.available = True + if not bridge.available: + _LOGGER.info('Reconnected to bridge %s', bridge.host) + bridge.available = True - new_lights = process_lights( - hass, api, bridge, - lambda **kw: update_lights(hass, bridge, add_devices, **kw)) - if bridge.allow_hue_groups: - new_lightgroups = process_groups( - hass, api, bridge, - lambda **kw: update_lights(hass, bridge, add_devices, **kw)) - new_lights.extend(new_lightgroups) + new_lights = [] + + for item_id in api: + if item_id not in current: + current[item_id] = HueLight( + api[item_id], request_bridge_update, bridge, is_group) + + new_lights.append(current[item_id]) + elif item_id not in progress_waiting: + current[item_id].async_schedule_update_ha_state() if new_lights: - add_devices(new_lights) - - -def process_lights(hass, api, bridge, update_lights_cb): - """Set up HueLight objects for all lights.""" - api_lights = api.get('lights') - - if not isinstance(api_lights, dict): - _LOGGER.error("Got unexpected result from Hue API") - return [] - - new_lights = [] - - for light_id, info in api_lights.items(): - if light_id not in bridge.lights: - bridge.lights[light_id] = HueLight( - int(light_id), info, bridge, - update_lights_cb, - bridge.allow_unreachable, - bridge.allow_in_emulated_hue) - new_lights.append(bridge.lights[light_id]) - else: - bridge.lights[light_id].info = info - hass.helpers.dispatcher.dispatcher_send( - SIGNAL_CALLBACK.format( - bridge.bridge_id, - bridge.lights[light_id].light_id)) - - return new_lights - - -def process_groups(hass, api, bridge, update_lights_cb): - """Set up HueLight objects for all groups.""" - api_groups = api.get('groups') - - if not isinstance(api_groups, dict): - _LOGGER.error('Got unexpected result from Hue API') - return [] - - new_lights = [] - - for lightgroup_id, info in api_groups.items(): - if 'state' not in info: - _LOGGER.warning( - "Group info does not contain state. Please update your hub") - return [] - - if lightgroup_id not in bridge.lightgroups: - bridge.lightgroups[lightgroup_id] = HueLight( - int(lightgroup_id), info, bridge, - update_lights_cb, - bridge.allow_unreachable, - bridge.allow_in_emulated_hue, True) - new_lights.append(bridge.lightgroups[lightgroup_id]) - else: - bridge.lightgroups[lightgroup_id].info = info - hass.helpers.dispatcher.dispatcher_send( - SIGNAL_CALLBACK.format( - bridge.bridge_id, - bridge.lightgroups[lightgroup_id].light_id)) - - return new_lights + async_add_devices(new_lights) class HueLight(Light): """Representation of a Hue light.""" - def __init__(self, light_id, info, bridge, update_lights_cb, - allow_unreachable, allow_in_emulated_hue, is_group=False): + def __init__(self, light, request_bridge_update, bridge, is_group=False): """Initialize the light.""" - self.light_id = light_id - self.info = info + self.light = light + self.async_request_bridge_update = request_bridge_update self.bridge = bridge - self.update_lights = update_lights_cb - self.allow_unreachable = allow_unreachable self.is_group = is_group - self.allow_in_emulated_hue = allow_in_emulated_hue if is_group: - self._command_func = self.bridge.set_group + self.is_osram = False + self.is_philips = False else: - self._command_func = self.bridge.set_light + self.is_osram = light.manufacturername == 'OSRAM' + self.is_philips = light.manufacturername == 'Philips' @property def unique_id(self): """Return the ID of this Hue light.""" - return self.info.get('uniqueid') + return self.light.uniqueid @property def name(self): """Return the name of the Hue light.""" - return self.info.get('name', DEVICE_DEFAULT_NAME) + return self.light.name @property def brightness(self): """Return the brightness of this light between 0..255.""" if self.is_group: - return self.info['action'].get('bri') - return self.info['state'].get('bri') + return self.light.action.get('bri') + return self.light.state.get('bri') @property def xy_color(self): """Return the XY color value.""" if self.is_group: - return self.info['action'].get('xy') - return self.info['state'].get('xy') + return self.light.action.get('xy') + return self.light.state.get('xy') @property def color_temp(self): """Return the CT color value.""" if self.is_group: - return self.info['action'].get('ct') - return self.info['state'].get('ct') + return self.light.action.get('ct') + return self.light.state.get('ct') @property def is_on(self): """Return true if device is on.""" if self.is_group: - return self.info['state']['any_on'] - return self.info['state']['on'] + return self.light.state['any_on'] + return self.light.state['on'] @property def available(self): """Return if light is available.""" return self.bridge.available and (self.is_group or - self.allow_unreachable or - self.info['state']['reachable']) + self.bridge.allow_unreachable or + self.light.state['reachable']) @property def supported_features(self): """Flag supported features.""" - return SUPPORT_HUE.get(self.info.get('type'), SUPPORT_HUE_EXTENDED) + return SUPPORT_HUE.get(self.light.type, SUPPORT_HUE_EXTENDED) @property def effect_list(self): """Return the list of supported effects.""" return [EFFECT_COLORLOOP, EFFECT_RANDOM] - def turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): """Turn the specified or all lights on.""" command = {'on': True} @@ -293,7 +273,7 @@ class HueLight(Light): command['transitiontime'] = int(kwargs[ATTR_TRANSITION] * 10) if ATTR_XY_COLOR in kwargs: - if self.info.get('manufacturername') == 'OSRAM': + if self.is_osram: color_hue, sat = color_util.color_xy_to_hs( *kwargs[ATTR_XY_COLOR]) command['hue'] = color_hue / 360 * 65535 @@ -301,7 +281,7 @@ class HueLight(Light): else: command['xy'] = kwargs[ATTR_XY_COLOR] elif ATTR_RGB_COLOR in kwargs: - if self.info.get('manufacturername') == 'OSRAM': + if self.is_osram: hsv = color_util.color_RGB_to_hsv( *(int(val) for val in kwargs[ATTR_RGB_COLOR])) command['hue'] = hsv[0] / 360 * 65535 @@ -336,12 +316,15 @@ class HueLight(Light): elif effect == EFFECT_RANDOM: command['hue'] = random.randrange(0, 65535) command['sat'] = random.randrange(150, 254) - elif self.info.get('manufacturername') == 'Philips': + elif self.is_philips: command['effect'] = 'none' - self._command_func(self.light_id, command) + if self.is_group: + await self.light.set_action(**command) + else: + await self.light.set_state(**command) - def turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs): """Turn the specified or all lights off.""" command = {'on': False} @@ -359,27 +342,19 @@ class HueLight(Light): else: command['alert'] = 'none' - self._command_func(self.light_id, command) + if self.is_group: + await self.light.set_action(**command) + else: + await self.light.set_state(**command) - def update(self): + async def async_update(self): """Synchronize state with bridge.""" - self.update_lights(no_throttle=True) + await self.async_request_bridge_update(self.is_group, self.light.id) @property def device_state_attributes(self): """Return the device state attributes.""" attributes = {} - if not self.allow_in_emulated_hue: - attributes[ATTR_EMULATED_HUE_HIDDEN] = \ - not self.allow_in_emulated_hue if self.is_group: attributes[ATTR_IS_HUE_GROUP] = self.is_group return attributes - - @asyncio.coroutine - def async_added_to_hass(self): - """Register update callback.""" - dev_id = self.bridge.bridge_id, self.light_id - self.hass.helpers.dispatcher.async_dispatcher_connect( - SIGNAL_CALLBACK.format(*dev_id), - self.async_schedule_update_ha_state) diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py index b6f6a1c5a92..d0164706626 100644 --- a/homeassistant/components/mqtt/discovery.py +++ b/homeassistant/components/mqtt/discovery.py @@ -4,7 +4,6 @@ Support for MQTT discovery. For more details about this component, please refer to the documentation at https://home-assistant.io/components/mqtt/#discovery """ -import asyncio import json import logging import re @@ -35,19 +34,16 @@ ALLOWED_PLATFORMS = { ALREADY_DISCOVERED = 'mqtt_discovered_components' -@asyncio.coroutine -def async_start(hass, discovery_topic, hass_config): +async def async_start(hass, discovery_topic, hass_config): """Initialize of MQTT Discovery.""" - # pylint: disable=unused-variable - @asyncio.coroutine - def async_device_message_received(topic, payload, qos): + async def async_device_message_received(topic, payload, qos): """Process the received message.""" match = TOPIC_MATCHER.match(topic) if not match: return - prefix_topic, component, node_id, object_id = match.groups() + _prefix_topic, component, node_id, object_id = match.groups() try: payload = json.loads(payload) @@ -88,10 +84,10 @@ def async_start(hass, discovery_topic, hass_config): _LOGGER.info("Found new component: %s %s", component, discovery_id) - yield from async_load_platform( + await async_load_platform( hass, component, platform, payload, hass_config) - yield from mqtt.async_subscribe( + await mqtt.async_subscribe( hass, discovery_topic + '/#', async_device_message_received, 0) return True diff --git a/homeassistant/components/zwave/__init__.py b/homeassistant/components/zwave/__init__.py index ad4ae66df17..43aa996c799 100644 --- a/homeassistant/components/zwave/__init__.py +++ b/homeassistant/components/zwave/__init__.py @@ -203,8 +203,8 @@ def get_config_value(node, value_index, tries=5): return None -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_devices, discovery_info=None): +async def async_setup_platform(hass, config, async_add_devices, + discovery_info=None): """Set up the Z-Wave platform (generic part).""" if discovery_info is None or DATA_NETWORK not in hass.data: return False @@ -504,8 +504,7 @@ def setup(hass, config): "target node:%s, instance=%s", node_id, group, target_node_id, instance) - @asyncio.coroutine - def async_refresh_entity(service): + async def async_refresh_entity(service): """Refresh values that specific entity depends on.""" entity_id = service.data.get(ATTR_ENTITY_ID) async_dispatcher_send( @@ -559,8 +558,7 @@ def setup(hass, config): network.start() hass.bus.fire(const.EVENT_NETWORK_START) - @asyncio.coroutine - def _check_awaked(): + async def _check_awaked(): """Wait for Z-wave awaked state (or timeout) and finalize start.""" _LOGGER.debug( "network state: %d %s", network.state, @@ -585,7 +583,7 @@ def setup(hass, config): network.state_str) break else: - yield from asyncio.sleep(1, loop=hass.loop) + await asyncio.sleep(1, loop=hass.loop) hass.async_add_job(_finalize_start) @@ -798,11 +796,10 @@ class ZWaveDeviceEntityValues(): dict_id = id(self) - @asyncio.coroutine - def discover_device(component, device, dict_id): + async def discover_device(component, device, dict_id): """Put device in a dictionary and call discovery on it.""" self._hass.data[DATA_DEVICES][dict_id] = device - yield from discovery.async_load_platform( + await discovery.async_load_platform( self._hass, component, DOMAIN, {const.DISCOVERY_DEVICE: dict_id}, self._zwave_config) self._hass.add_job(discover_device, component, device, dict_id) @@ -844,8 +841,7 @@ class ZWaveDeviceEntity(ZWaveBaseEntity): self.update_properties() self.maybe_schedule_update() - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Add device to dict.""" async_dispatcher_connect( self.hass, diff --git a/homeassistant/core.py b/homeassistant/core.py index b49b94f853d..65db82a1fbe 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -79,7 +79,7 @@ def callback(func: Callable[..., None]) -> Callable[..., None]: def is_callback(func: Callable[..., Any]) -> bool: """Check if function is safe to be called in the event loop.""" - return '_hass_callback' in func.__dict__ + return '_hass_callback' in getattr(func, '__dict__', {}) @callback diff --git a/homeassistant/helpers/discovery.py b/homeassistant/helpers/discovery.py index 82322fec1e5..cb587c432c1 100644 --- a/homeassistant/helpers/discovery.py +++ b/homeassistant/helpers/discovery.py @@ -5,8 +5,6 @@ There are two different types of discoveries that can be fired/listened for. - listen_platform/discover_platform is for platforms. These are used by components to allow discovery of their platforms. """ -import asyncio - from homeassistant import setup, core from homeassistant.loader import bind_hass from homeassistant.const import ( @@ -58,17 +56,16 @@ def discover(hass, service, discovered=None, component=None, hass_config=None): async_discover(hass, service, discovered, component, hass_config)) -@asyncio.coroutine @bind_hass -def async_discover(hass, service, discovered=None, component=None, - hass_config=None): +async def async_discover(hass, service, discovered=None, component=None, + hass_config=None): """Fire discovery event. Can ensure a component is loaded.""" if component in DEPENDENCY_BLACKLIST: raise HomeAssistantError( 'Cannot discover the {} component.'.format(component)) if component is not None and component not in hass.config.components: - yield from setup.async_setup_component( + await setup.async_setup_component( hass, component, hass_config) data = { @@ -134,10 +131,9 @@ def load_platform(hass, component, platform, discovered=None, hass_config)) -@asyncio.coroutine @bind_hass -def async_load_platform(hass, component, platform, discovered=None, - hass_config=None): +async def async_load_platform(hass, component, platform, discovered=None, + hass_config=None): """Load a component and platform dynamically. Target components will be loaded and an EVENT_PLATFORM_DISCOVERED will be @@ -148,7 +144,7 @@ def async_load_platform(hass, component, platform, discovered=None, Use `listen_platform` to register a callback for these events. - Warning: Do not yield from this inside a setup method to avoid a dead lock. + Warning: Do not await this inside a setup method to avoid a dead lock. Use `hass.async_add_job(async_load_platform(..))` instead. This method is a coroutine. @@ -160,7 +156,7 @@ def async_load_platform(hass, component, platform, discovered=None, setup_success = True if component not in hass.config.components: - setup_success = yield from setup.async_setup_component( + setup_success = await setup.async_setup_component( hass, component, hass_config) # No need to fire event if we could not setup component diff --git a/homeassistant/util/__init__.py b/homeassistant/util/__init__.py index a869251dc3c..82ba6a734f8 100644 --- a/homeassistant/util/__init__.py +++ b/homeassistant/util/__init__.py @@ -261,6 +261,16 @@ class Throttle(object): def __call__(self, method): """Caller for the throttle.""" + # Make sure we return a coroutine if the method is async. + if asyncio.iscoroutinefunction(method): + async def throttled_value(): + """Stand-in function for when real func is being throttled.""" + return None + else: + def throttled_value(): + """Stand-in function for when real func is being throttled.""" + return None + if self.limit_no_throttle is not None: method = Throttle(self.limit_no_throttle)(method) @@ -277,16 +287,6 @@ class Throttle(object): is_func = (not hasattr(method, '__self__') and '.' not in method.__qualname__.split('..')[-1]) - # Make sure we return a coroutine if the method is async. - if asyncio.iscoroutinefunction(method): - async def throttled_value(): - """Stand-in function for when real func is being throttled.""" - return None - else: - def throttled_value(): - """Stand-in function for when real func is being throttled.""" - return None - @wraps(method) def wrapper(*args, **kwargs): """Wrap that allows wrapped to be called only once per min_time. diff --git a/requirements_all.txt b/requirements_all.txt index f25200ba49e..839987611bc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -74,7 +74,7 @@ aiodns==1.1.1 aiohttp_cors==0.6.0 # homeassistant.components.hue -aiohue==0.3.0 +aiohue==1.2.0 # homeassistant.components.sensor.imap aioimaplib==0.7.13 @@ -568,9 +568,6 @@ pdunehd==1.3 # homeassistant.components.media_player.pandora pexpect==4.0.1 -# homeassistant.components.hue -phue==1.0 - # homeassistant.components.rpi_pfio pifacecommon==4.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8b3cc8d207a..d41f9589de2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -35,7 +35,7 @@ aioautomatic==0.6.5 aiohttp_cors==0.6.0 # homeassistant.components.hue -aiohue==0.3.0 +aiohue==1.2.0 # homeassistant.components.notify.apns apns2==0.3.0 diff --git a/tests/components/hue/__init__.py b/tests/components/hue/__init__.py new file mode 100644 index 00000000000..8cff8700aaf --- /dev/null +++ b/tests/components/hue/__init__.py @@ -0,0 +1 @@ +"""Tests for the Hue component.""" diff --git a/tests/components/hue/conftest.py b/tests/components/hue/conftest.py new file mode 100644 index 00000000000..7ccc202b31b --- /dev/null +++ b/tests/components/hue/conftest.py @@ -0,0 +1,17 @@ +"""Fixtures for Hue tests.""" +from unittest.mock import patch + +import pytest + +from tests.common import mock_coro_func + + +@pytest.fixture +def mock_bridge(): + """Mock the HueBridge from initializing.""" + with patch('homeassistant.components.hue._find_username_from_config', + return_value=None), \ + patch('homeassistant.components.hue.HueBridge') as mock_bridge: + mock_bridge().async_setup = mock_coro_func() + mock_bridge.reset_mock() + yield mock_bridge diff --git a/tests/components/hue/test_bridge.py b/tests/components/hue/test_bridge.py new file mode 100644 index 00000000000..88a7223d91e --- /dev/null +++ b/tests/components/hue/test_bridge.py @@ -0,0 +1,98 @@ +"""Test Hue bridge.""" +import asyncio +from unittest.mock import Mock, patch + +import aiohue +import pytest + +from homeassistant.components import hue + +from tests.common import mock_coro + + +class MockBridge(hue.HueBridge): + """Class that sets default for constructor.""" + + def __init__(self, hass, host='1.2.3.4', filename='mock-bridge.conf', + username=None, **kwargs): + """Initialize a mock bridge.""" + super().__init__(host, hass, filename, username, **kwargs) + + +@pytest.fixture +def mock_request(): + """Mock configurator.async_request_config.""" + with patch('homeassistant.components.configurator.' + 'async_request_config') as mock_request: + yield mock_request + + +async def test_setup_request_config_button_not_pressed(hass, mock_request): + """Test we request config if link button has not been pressed.""" + with patch('aiohue.Bridge.create_user', + side_effect=aiohue.LinkButtonNotPressed): + await MockBridge(hass).async_setup() + + assert len(mock_request.mock_calls) == 1 + + +async def test_setup_request_config_invalid_username(hass, mock_request): + """Test we request config if username is no longer whitelisted.""" + with patch('aiohue.Bridge.create_user', + side_effect=aiohue.Unauthorized): + await MockBridge(hass).async_setup() + + assert len(mock_request.mock_calls) == 1 + + +async def test_setup_timeout(hass, mock_request): + """Test we give up when there is a timeout.""" + with patch('aiohue.Bridge.create_user', + side_effect=asyncio.TimeoutError): + await MockBridge(hass).async_setup() + + assert len(mock_request.mock_calls) == 0 + + +async def test_only_create_no_username(hass): + """.""" + with patch('aiohue.Bridge.create_user') as mock_create, \ + patch('aiohue.Bridge.initialize') as mock_init: + await MockBridge(hass, username='bla').async_setup() + + assert len(mock_create.mock_calls) == 0 + assert len(mock_init.mock_calls) == 1 + + +async def test_configurator_callback(hass, mock_request): + """.""" + with patch('aiohue.Bridge.create_user', + side_effect=aiohue.LinkButtonNotPressed): + await MockBridge(hass).async_setup() + + assert len(mock_request.mock_calls) == 1 + + callback = mock_request.mock_calls[0][1][2] + + mock_init = Mock(return_value=mock_coro()) + mock_create = Mock(return_value=mock_coro()) + + with patch('aiohue.Bridge') as mock_bridge, \ + patch('homeassistant.helpers.discovery.async_load_platform', + return_value=mock_coro()) as mock_load_platform, \ + patch('homeassistant.components.hue.save_json') as mock_save: + inst = mock_bridge() + inst.username = 'mock-user' + inst.create_user = mock_create + inst.initialize = mock_init + await callback(None) + + assert len(mock_create.mock_calls) == 1 + assert len(mock_init.mock_calls) == 1 + assert len(mock_save.mock_calls) == 1 + assert mock_save.mock_calls[0][1][1] == { + '1.2.3.4': { + 'username': 'mock-user' + } + } + assert len(mock_load_platform.mock_calls) == 1 diff --git a/tests/components/hue/test_config_flow.py b/tests/components/hue/test_config_flow.py new file mode 100644 index 00000000000..959e3c6241b --- /dev/null +++ b/tests/components/hue/test_config_flow.py @@ -0,0 +1,184 @@ +"""Tests for Philips Hue config flow.""" +import asyncio +from unittest.mock import patch + +import aiohue +import pytest +import voluptuous as vol + +from homeassistant.components import hue + +from tests.common import MockConfigEntry, mock_coro + + +async def test_flow_works(hass, aioclient_mock): + """Test config flow .""" + aioclient_mock.get(hue.API_NUPNP, json=[ + {'internalipaddress': '1.2.3.4', 'id': 'bla'} + ]) + + flow = hue.HueFlowHandler() + flow.hass = hass + await flow.async_step_init() + + with patch('aiohue.Bridge') as mock_bridge: + def mock_constructor(host, websession): + mock_bridge.host = host + return mock_bridge + + mock_bridge.side_effect = mock_constructor + mock_bridge.username = 'username-abc' + mock_bridge.config.name = 'Mock Bridge' + mock_bridge.config.bridgeid = 'bridge-id-1234' + mock_bridge.create_user.return_value = mock_coro() + mock_bridge.initialize.return_value = mock_coro() + + result = await flow.async_step_link(user_input={}) + + assert mock_bridge.host == '1.2.3.4' + assert len(mock_bridge.create_user.mock_calls) == 1 + assert len(mock_bridge.initialize.mock_calls) == 1 + + assert result['type'] == 'create_entry' + assert result['title'] == 'Mock Bridge' + assert result['data'] == { + 'host': '1.2.3.4', + 'bridge_id': 'bridge-id-1234', + 'username': 'username-abc' + } + + +async def test_flow_no_discovered_bridges(hass, aioclient_mock): + """Test config flow discovers no bridges.""" + aioclient_mock.get(hue.API_NUPNP, json=[]) + flow = hue.HueFlowHandler() + flow.hass = hass + + result = await flow.async_step_init() + assert result['type'] == 'abort' + + +async def test_flow_all_discovered_bridges_exist(hass, aioclient_mock): + """Test config flow discovers only already configured bridges.""" + aioclient_mock.get(hue.API_NUPNP, json=[ + {'internalipaddress': '1.2.3.4', 'id': 'bla'} + ]) + MockConfigEntry(domain='hue', data={ + 'host': '1.2.3.4' + }).add_to_hass(hass) + flow = hue.HueFlowHandler() + flow.hass = hass + + result = await flow.async_step_init() + assert result['type'] == 'abort' + + +async def test_flow_one_bridge_discovered(hass, aioclient_mock): + """Test config flow discovers one bridge.""" + aioclient_mock.get(hue.API_NUPNP, json=[ + {'internalipaddress': '1.2.3.4', 'id': 'bla'} + ]) + flow = hue.HueFlowHandler() + flow.hass = hass + + result = await flow.async_step_init() + assert result['type'] == 'form' + assert result['step_id'] == 'link' + + +async def test_flow_two_bridges_discovered(hass, aioclient_mock): + """Test config flow discovers two bridges.""" + aioclient_mock.get(hue.API_NUPNP, json=[ + {'internalipaddress': '1.2.3.4', 'id': 'bla'}, + {'internalipaddress': '5.6.7.8', 'id': 'beer'} + ]) + flow = hue.HueFlowHandler() + flow.hass = hass + + result = await flow.async_step_init() + assert result['type'] == 'form' + assert result['step_id'] == 'init' + + with pytest.raises(vol.Invalid): + assert result['data_schema']({'host': '0.0.0.0'}) + + result['data_schema']({'host': '1.2.3.4'}) + result['data_schema']({'host': '5.6.7.8'}) + + +async def test_flow_two_bridges_discovered_one_new(hass, aioclient_mock): + """Test config flow discovers two bridges.""" + aioclient_mock.get(hue.API_NUPNP, json=[ + {'internalipaddress': '1.2.3.4', 'id': 'bla'}, + {'internalipaddress': '5.6.7.8', 'id': 'beer'} + ]) + MockConfigEntry(domain='hue', data={ + 'host': '1.2.3.4' + }).add_to_hass(hass) + flow = hue.HueFlowHandler() + flow.hass = hass + + result = await flow.async_step_init() + assert result['type'] == 'form' + assert result['step_id'] == 'link' + assert flow.host == '5.6.7.8' + + +async def test_flow_timeout_discovery(hass): + """Test config flow .""" + flow = hue.HueFlowHandler() + flow.hass = hass + + with patch('aiohue.discovery.discover_nupnp', + side_effect=asyncio.TimeoutError): + result = await flow.async_step_init() + + assert result['type'] == 'abort' + + +async def test_flow_link_timeout(hass): + """Test config flow .""" + flow = hue.HueFlowHandler() + flow.hass = hass + + with patch('aiohue.Bridge.create_user', + side_effect=asyncio.TimeoutError): + result = await flow.async_step_link({}) + + assert result['type'] == 'form' + assert result['step_id'] == 'link' + assert result['errors'] == { + 'base': 'register_failed' + } + + +async def test_flow_link_button_not_pressed(hass): + """Test config flow .""" + flow = hue.HueFlowHandler() + flow.hass = hass + + with patch('aiohue.Bridge.create_user', + side_effect=aiohue.LinkButtonNotPressed): + result = await flow.async_step_link({}) + + assert result['type'] == 'form' + assert result['step_id'] == 'link' + assert result['errors'] == { + 'base': 'register_failed' + } + + +async def test_flow_link_unknown_host(hass): + """Test config flow .""" + flow = hue.HueFlowHandler() + flow.hass = hass + + with patch('aiohue.Bridge.create_user', + side_effect=aiohue.RequestError): + result = await flow.async_step_link({}) + + assert result['type'] == 'form' + assert result['step_id'] == 'link' + assert result['errors'] == { + 'base': 'register_failed' + } diff --git a/tests/components/hue/test_setup.py b/tests/components/hue/test_setup.py new file mode 100644 index 00000000000..690419fcb7a --- /dev/null +++ b/tests/components/hue/test_setup.py @@ -0,0 +1,74 @@ +"""Test Hue setup process.""" +from homeassistant.setup import async_setup_component +from homeassistant.components import hue +from homeassistant.components.discovery import SERVICE_HUE + + +async def test_setup_with_multiple_hosts(hass, mock_bridge): + """Multiple hosts specified in the config file.""" + assert await async_setup_component(hass, hue.DOMAIN, { + hue.DOMAIN: { + hue.CONF_BRIDGES: [ + {hue.CONF_HOST: '127.0.0.1'}, + {hue.CONF_HOST: '192.168.1.10'}, + ] + } + }) + + assert len(mock_bridge.mock_calls) == 2 + hosts = sorted(mock_call[1][0] for mock_call in mock_bridge.mock_calls) + assert hosts == ['127.0.0.1', '192.168.1.10'] + assert len(hass.data[hue.DOMAIN]) == 2 + + +async def test_bridge_discovered(hass, mock_bridge): + """Bridge discovery.""" + assert await async_setup_component(hass, hue.DOMAIN, {}) + + await hass.helpers.discovery.async_discover(SERVICE_HUE, { + 'host': '192.168.1.10', + 'serial': '1234567', + }) + await hass.async_block_till_done() + + assert len(mock_bridge.mock_calls) == 1 + assert mock_bridge.mock_calls[0][1][0] == '192.168.1.10' + assert len(hass.data[hue.DOMAIN]) == 1 + + +async def test_bridge_configure_and_discovered(hass, mock_bridge): + """Bridge is in the config file, then we discover it.""" + assert await async_setup_component(hass, hue.DOMAIN, { + hue.DOMAIN: { + hue.CONF_BRIDGES: { + hue.CONF_HOST: '192.168.1.10' + } + } + }) + + assert len(mock_bridge.mock_calls) == 1 + assert mock_bridge.mock_calls[0][1][0] == '192.168.1.10' + assert len(hass.data[hue.DOMAIN]) == 1 + + mock_bridge.reset_mock() + + await hass.helpers.discovery.async_discover(SERVICE_HUE, { + 'host': '192.168.1.10', + 'serial': '1234567', + }) + await hass.async_block_till_done() + + assert len(mock_bridge.mock_calls) == 0 + assert len(hass.data[hue.DOMAIN]) == 1 + + +async def test_setup_no_host(hass, aioclient_mock): + """Check we call discovery if domain specified but no bridges.""" + aioclient_mock.get(hue.API_NUPNP, json=[]) + + result = await async_setup_component( + hass, hue.DOMAIN, {hue.DOMAIN: {}}) + assert result + + assert len(aioclient_mock.mock_calls) == 1 + assert len(hass.data[hue.DOMAIN]) == 0 diff --git a/tests/components/light/test_hue.py b/tests/components/light/test_hue.py index 559467d5e9a..8abf51fdf0c 100644 --- a/tests/components/light/test_hue.py +++ b/tests/components/light/test_hue.py @@ -1,545 +1,590 @@ """Philips Hue lights platform tests.""" - +import asyncio +from collections import deque import logging -import unittest -import unittest.mock as mock -from unittest.mock import call, MagicMock, patch +from unittest.mock import Mock + +import aiohue +from aiohue.lights import Lights +from aiohue.groups import Groups +import pytest from homeassistant.components import hue import homeassistant.components.light.hue as hue_light -from tests.common import get_test_home_assistant, MockDependency - _LOGGER = logging.getLogger(__name__) HUE_LIGHT_NS = 'homeassistant.components.light.hue.' - - -class TestSetup(unittest.TestCase): - """Test the Hue light platform.""" - - def setUp(self): # pylint: disable=invalid-name - """Setup things to be run when tests are started.""" - self.hass = get_test_home_assistant() - self.skip_teardown_stop = False - - def tearDown(self): - """Stop everything that was started.""" - if not self.skip_teardown_stop: - self.hass.stop() - - def setup_mocks_for_update_lights(self): - """Set up all mocks for update_lights tests.""" - self.mock_bridge = MagicMock() - self.mock_bridge.bridge_id = 'bridge-id' - self.mock_bridge.allow_hue_groups = False - self.mock_api = MagicMock() - self.mock_bridge.get_api.return_value = self.mock_api - self.mock_add_devices = MagicMock() - - def setup_mocks_for_process_lights(self): - """Set up all mocks for process_lights tests.""" - self.mock_bridge = self.create_mock_bridge('host') - self.mock_api = MagicMock() - self.mock_api.get.return_value = {} - self.mock_bridge.get_api.return_value = self.mock_api - - def setup_mocks_for_process_groups(self): - """Set up all mocks for process_groups tests.""" - self.mock_bridge = self.create_mock_bridge('host') - self.mock_bridge.get_group.return_value = { - 'name': 'Group 0', 'state': {'any_on': True}} - - self.mock_api = MagicMock() - self.mock_api.get.return_value = {} - self.mock_bridge.get_api.return_value = self.mock_api - - def create_mock_bridge(self, host, allow_hue_groups=True): - """Return a mock HueBridge with reasonable defaults.""" - mock_bridge = MagicMock() - mock_bridge.bridge_id = 'bridge-id' - mock_bridge.host = host - mock_bridge.allow_hue_groups = allow_hue_groups - mock_bridge.lights = {} - mock_bridge.lightgroups = {} - return mock_bridge - - def create_mock_lights(self, lights): - """Return a dict suitable for mocking api.get('lights').""" - mock_bridge_lights = lights - - for info in mock_bridge_lights.values(): - if 'state' not in info: - info['state'] = {'on': False} - - return mock_bridge_lights - - def build_mock_light(self, bridge, light_id, name): - """Return a mock HueLight.""" - light = MagicMock() - light.bridge = bridge - light.light_id = light_id - light.name = name - return light - - def test_setup_platform_no_discovery_info(self): - """Test setup_platform without discovery info.""" - self.hass.data[hue.DOMAIN] = {} - mock_add_devices = MagicMock() - - hue_light.setup_platform(self.hass, {}, mock_add_devices) - - mock_add_devices.assert_not_called() - - def test_setup_platform_no_bridge_id(self): - """Test setup_platform without a bridge.""" - self.hass.data[hue.DOMAIN] = {} - mock_add_devices = MagicMock() - - hue_light.setup_platform(self.hass, {}, mock_add_devices, {}) - - mock_add_devices.assert_not_called() - - def test_setup_platform_one_bridge(self): - """Test setup_platform with one bridge.""" - mock_bridge = MagicMock() - self.hass.data[hue.DOMAIN] = {'10.0.0.1': mock_bridge} - mock_add_devices = MagicMock() - - with patch(HUE_LIGHT_NS + 'unthrottled_update_lights') \ - as mock_update_lights: - hue_light.setup_platform( - self.hass, {}, mock_add_devices, - {'bridge_id': '10.0.0.1'}) - mock_update_lights.assert_called_once_with( - self.hass, mock_bridge, mock_add_devices) - - def test_setup_platform_multiple_bridges(self): - """Test setup_platform wuth multiple bridges.""" - mock_bridge = MagicMock() - mock_bridge2 = MagicMock() - self.hass.data[hue.DOMAIN] = { - '10.0.0.1': mock_bridge, - '192.168.0.10': mock_bridge2, +GROUP_RESPONSE = { + "1": { + "name": "Group 1", + "lights": [ + "1", + "2" + ], + "type": "LightGroup", + "action": { + "on": True, + "bri": 254, + "hue": 10000, + "sat": 254, + "effect": "none", + "xy": [ + 0.5, + 0.5 + ], + "ct": 250, + "alert": "select", + "colormode": "ct" + }, + "state": { + "any_on": True, + "all_on": False, } - mock_add_devices = MagicMock() - - with patch(HUE_LIGHT_NS + 'unthrottled_update_lights') \ - as mock_update_lights: - hue_light.setup_platform( - self.hass, {}, mock_add_devices, - {'bridge_id': '10.0.0.1'}) - hue_light.setup_platform( - self.hass, {}, mock_add_devices, - {'bridge_id': '192.168.0.10'}) - - mock_update_lights.assert_has_calls([ - call(self.hass, mock_bridge, mock_add_devices), - call(self.hass, mock_bridge2, mock_add_devices), - ]) - - @MockDependency('phue') - def test_update_lights_with_no_lights(self, mock_phue): - """Test the update_lights function when no lights are found.""" - self.setup_mocks_for_update_lights() - - with patch(HUE_LIGHT_NS + 'process_lights', return_value=[]) \ - as mock_process_lights: - with patch(HUE_LIGHT_NS + 'process_groups', return_value=[]) \ - as mock_process_groups: - with patch.object(self.hass.helpers.dispatcher, - 'dispatcher_send') as dispatcher_send: - hue_light.unthrottled_update_lights( - self.hass, self.mock_bridge, self.mock_add_devices) - - mock_process_lights.assert_called_once_with( - self.hass, self.mock_api, self.mock_bridge, mock.ANY) - mock_process_groups.assert_not_called() - self.mock_add_devices.assert_not_called() - dispatcher_send.assert_not_called() - - @MockDependency('phue') - def test_update_lights_with_some_lights(self, mock_phue): - """Test the update_lights function with some lights.""" - self.setup_mocks_for_update_lights() - mock_lights = [ - self.build_mock_light(self.mock_bridge, 42, 'some'), - self.build_mock_light(self.mock_bridge, 84, 'light'), - ] - - with patch(HUE_LIGHT_NS + 'process_lights', - return_value=mock_lights) as mock_process_lights: - with patch(HUE_LIGHT_NS + 'process_groups', return_value=[]) \ - as mock_process_groups: - with patch.object(self.hass.helpers.dispatcher, - 'dispatcher_send') as dispatcher_send: - hue_light.unthrottled_update_lights( - self.hass, self.mock_bridge, self.mock_add_devices) - - mock_process_lights.assert_called_once_with( - self.hass, self.mock_api, self.mock_bridge, mock.ANY) - mock_process_groups.assert_not_called() - self.mock_add_devices.assert_called_once_with( - mock_lights) - dispatcher_send.assert_not_called() - - @MockDependency('phue') - def test_update_lights_no_groups(self, mock_phue): - """Test the update_lights function when no groups are found.""" - self.setup_mocks_for_update_lights() - self.mock_bridge.allow_hue_groups = True - mock_lights = [ - self.build_mock_light(self.mock_bridge, 42, 'some'), - self.build_mock_light(self.mock_bridge, 84, 'light'), - ] - - with patch(HUE_LIGHT_NS + 'process_lights', - return_value=mock_lights) as mock_process_lights: - with patch(HUE_LIGHT_NS + 'process_groups', return_value=[]) \ - as mock_process_groups: - with patch.object(self.hass.helpers.dispatcher, - 'dispatcher_send') as dispatcher_send: - hue_light.unthrottled_update_lights( - self.hass, self.mock_bridge, self.mock_add_devices) - - mock_process_lights.assert_called_once_with( - self.hass, self.mock_api, self.mock_bridge, mock.ANY) - mock_process_groups.assert_called_once_with( - self.hass, self.mock_api, self.mock_bridge, mock.ANY) - self.mock_add_devices.assert_called_once_with( - mock_lights) - dispatcher_send.assert_not_called() - - @MockDependency('phue') - def test_update_lights_with_lights_and_groups(self, mock_phue): - """Test the update_lights function with both lights and groups.""" - self.setup_mocks_for_update_lights() - self.mock_bridge.allow_hue_groups = True - mock_lights = [ - self.build_mock_light(self.mock_bridge, 42, 'some'), - self.build_mock_light(self.mock_bridge, 84, 'light'), - ] - mock_groups = [ - self.build_mock_light(self.mock_bridge, 15, 'and'), - self.build_mock_light(self.mock_bridge, 72, 'groups'), - ] - - with patch(HUE_LIGHT_NS + 'process_lights', - return_value=mock_lights) as mock_process_lights: - with patch(HUE_LIGHT_NS + 'process_groups', - return_value=mock_groups) as mock_process_groups: - with patch.object(self.hass.helpers.dispatcher, - 'dispatcher_send') as dispatcher_send: - hue_light.unthrottled_update_lights( - self.hass, self.mock_bridge, self.mock_add_devices) - - mock_process_lights.assert_called_once_with( - self.hass, self.mock_api, self.mock_bridge, mock.ANY) - mock_process_groups.assert_called_once_with( - self.hass, self.mock_api, self.mock_bridge, mock.ANY) - # note that mock_lights has been modified in place and - # now contains both lights and groups - self.mock_add_devices.assert_called_once_with( - mock_lights) - dispatcher_send.assert_not_called() - - @MockDependency('phue') - def test_update_lights_with_two_bridges(self, mock_phue): - """Test the update_lights function with two bridges.""" - self.setup_mocks_for_update_lights() - - mock_bridge_one = self.create_mock_bridge('one', False) - mock_bridge_one_lights = self.create_mock_lights( - {1: {'name': 'b1l1'}, 2: {'name': 'b1l2'}}) - - mock_bridge_two = self.create_mock_bridge('two', False) - mock_bridge_two_lights = self.create_mock_lights( - {1: {'name': 'b2l1'}, 3: {'name': 'b2l3'}}) - - with patch('homeassistant.components.light.hue.HueLight.' - 'schedule_update_ha_state'): - mock_api = MagicMock() - mock_api.get.return_value = mock_bridge_one_lights - with patch.object(mock_bridge_one, 'get_api', - return_value=mock_api): - hue_light.unthrottled_update_lights( - self.hass, mock_bridge_one, self.mock_add_devices) - - mock_api = MagicMock() - mock_api.get.return_value = mock_bridge_two_lights - with patch.object(mock_bridge_two, 'get_api', - return_value=mock_api): - hue_light.unthrottled_update_lights( - self.hass, mock_bridge_two, self.mock_add_devices) - - self.assertEqual(sorted(mock_bridge_one.lights.keys()), [1, 2]) - self.assertEqual(sorted(mock_bridge_two.lights.keys()), [1, 3]) - - self.assertEqual(len(self.mock_add_devices.mock_calls), 2) - - # first call - name, args, kwargs = self.mock_add_devices.mock_calls[0] - self.assertEqual(len(args), 1) - self.assertEqual(len(kwargs), 0) - - # second call works the same - name, args, kwargs = self.mock_add_devices.mock_calls[1] - self.assertEqual(len(args), 1) - self.assertEqual(len(kwargs), 0) - - def test_process_lights_api_error(self): - """Test the process_lights function when the bridge errors out.""" - self.setup_mocks_for_process_lights() - self.mock_api.get.return_value = None - - ret = hue_light.process_lights( - self.hass, self.mock_api, self.mock_bridge, None) - - self.assertEqual([], ret) - self.assertEqual(self.mock_bridge.lights, {}) - - def test_process_lights_no_lights(self): - """Test the process_lights function when bridge returns no lights.""" - self.setup_mocks_for_process_lights() - - with patch.object(self.hass.helpers.dispatcher, 'dispatcher_send') \ - as mock_dispatcher_send: - ret = hue_light.process_lights( - self.hass, self.mock_api, self.mock_bridge, None) - - self.assertEqual([], ret) - mock_dispatcher_send.assert_not_called() - self.assertEqual(self.mock_bridge.lights, {}) - - @patch(HUE_LIGHT_NS + 'HueLight') - def test_process_lights_some_lights(self, mock_hue_light): - """Test the process_lights function with multiple groups.""" - self.setup_mocks_for_process_lights() - self.mock_api.get.return_value = { - 1: {'state': 'on'}, 2: {'state': 'off'}} - - with patch.object(self.hass.helpers.dispatcher, 'dispatcher_send') \ - as mock_dispatcher_send: - ret = hue_light.process_lights( - self.hass, self.mock_api, self.mock_bridge, None) - - self.assertEqual(len(ret), 2) - mock_hue_light.assert_has_calls([ - call( - 1, {'state': 'on'}, self.mock_bridge, mock.ANY, - self.mock_bridge.allow_unreachable, - self.mock_bridge.allow_in_emulated_hue), - call( - 2, {'state': 'off'}, self.mock_bridge, mock.ANY, - self.mock_bridge.allow_unreachable, - self.mock_bridge.allow_in_emulated_hue), - ]) - mock_dispatcher_send.assert_not_called() - self.assertEqual(len(self.mock_bridge.lights), 2) - - @patch(HUE_LIGHT_NS + 'HueLight') - def test_process_lights_new_light(self, mock_hue_light): - """ - Test the process_lights function with new groups. - - Test what happens when we already have a light and a new one shows up. - """ - self.setup_mocks_for_process_lights() - self.mock_api.get.return_value = { - 1: {'state': 'on'}, 2: {'state': 'off'}} - self.mock_bridge.lights = { - 1: self.build_mock_light(self.mock_bridge, 1, 'foo')} - - with patch.object(self.hass.helpers.dispatcher, 'dispatcher_send') \ - as mock_dispatcher_send: - ret = hue_light.process_lights( - self.hass, self.mock_api, self.mock_bridge, None) - - self.assertEqual(len(ret), 1) - mock_hue_light.assert_has_calls([ - call( - 2, {'state': 'off'}, self.mock_bridge, mock.ANY, - self.mock_bridge.allow_unreachable, - self.mock_bridge.allow_in_emulated_hue), - ]) - mock_dispatcher_send.assert_called_once_with( - 'hue_light_callback_bridge-id_1') - self.assertEqual(len(self.mock_bridge.lights), 2) - - def test_process_groups_api_error(self): - """Test the process_groups function when the bridge errors out.""" - self.setup_mocks_for_process_groups() - self.mock_api.get.return_value = None - - ret = hue_light.process_groups( - self.hass, self.mock_api, self.mock_bridge, None) - - self.assertEqual([], ret) - self.assertEqual(self.mock_bridge.lightgroups, {}) - - def test_process_groups_no_state(self): - """Test the process_groups function when bridge returns no status.""" - self.setup_mocks_for_process_groups() - self.mock_bridge.get_group.return_value = {'name': 'Group 0'} - - with patch.object(self.hass.helpers.dispatcher, 'dispatcher_send') \ - as mock_dispatcher_send: - ret = hue_light.process_groups( - self.hass, self.mock_api, self.mock_bridge, None) - - self.assertEqual([], ret) - mock_dispatcher_send.assert_not_called() - self.assertEqual(self.mock_bridge.lightgroups, {}) - - @patch(HUE_LIGHT_NS + 'HueLight') - def test_process_groups_some_groups(self, mock_hue_light): - """Test the process_groups function with multiple groups.""" - self.setup_mocks_for_process_groups() - self.mock_api.get.return_value = { - 1: {'state': 'on'}, 2: {'state': 'off'}} - - with patch.object(self.hass.helpers.dispatcher, 'dispatcher_send') \ - as mock_dispatcher_send: - ret = hue_light.process_groups( - self.hass, self.mock_api, self.mock_bridge, None) - - self.assertEqual(len(ret), 2) - mock_hue_light.assert_has_calls([ - call( - 1, {'state': 'on'}, self.mock_bridge, mock.ANY, - self.mock_bridge.allow_unreachable, - self.mock_bridge.allow_in_emulated_hue, True), - call( - 2, {'state': 'off'}, self.mock_bridge, mock.ANY, - self.mock_bridge.allow_unreachable, - self.mock_bridge.allow_in_emulated_hue, True), - ]) - mock_dispatcher_send.assert_not_called() - self.assertEqual(len(self.mock_bridge.lightgroups), 2) - - @patch(HUE_LIGHT_NS + 'HueLight') - def test_process_groups_new_group(self, mock_hue_light): - """ - Test the process_groups function with new groups. - - Test what happens when we already have a light and a new one shows up. - """ - self.setup_mocks_for_process_groups() - self.mock_api.get.return_value = { - 1: {'state': 'on'}, 2: {'state': 'off'}} - self.mock_bridge.lightgroups = { - 1: self.build_mock_light(self.mock_bridge, 1, 'foo')} - - with patch.object(self.hass.helpers.dispatcher, 'dispatcher_send') \ - as mock_dispatcher_send: - ret = hue_light.process_groups( - self.hass, self.mock_api, self.mock_bridge, None) - - self.assertEqual(len(ret), 1) - mock_hue_light.assert_has_calls([ - call( - 2, {'state': 'off'}, self.mock_bridge, mock.ANY, - self.mock_bridge.allow_unreachable, - self.mock_bridge.allow_in_emulated_hue, True), - ]) - mock_dispatcher_send.assert_called_once_with( - 'hue_light_callback_bridge-id_1') - self.assertEqual(len(self.mock_bridge.lightgroups), 2) + }, + "2": { + "name": "Group 2", + "lights": [ + "3", + "4", + "5" + ], + "type": "LightGroup", + "action": { + "on": True, + "bri": 153, + "hue": 4345, + "sat": 254, + "effect": "none", + "xy": [ + 0.5, + 0.5 + ], + "ct": 250, + "alert": "select", + "colormode": "ct" + }, + "state": { + "any_on": True, + "all_on": False, + } + } +} +LIGHT_1_ON = { + "state": { + "on": True, + "bri": 144, + "hue": 13088, + "sat": 212, + "xy": [0.5128, 0.4147], + "ct": 467, + "alert": "none", + "effect": "none", + "colormode": "xy", + "reachable": True + }, + "type": "Extended color light", + "name": "Hue Lamp 1", + "modelid": "LCT001", + "swversion": "66009461", + "manufacturername": "Philips", + "uniqueid": "456", +} +LIGHT_1_OFF = { + "state": { + "on": False, + "bri": 0, + "hue": 0, + "sat": 0, + "xy": [0, 0], + "ct": 0, + "alert": "none", + "effect": "none", + "colormode": "xy", + "reachable": True + }, + "type": "Extended color light", + "name": "Hue Lamp 1", + "modelid": "LCT001", + "swversion": "66009461", + "manufacturername": "Philips", + "uniqueid": "456", +} +LIGHT_2_OFF = { + "state": { + "on": False, + "bri": 0, + "hue": 0, + "sat": 0, + "xy": [0, 0], + "ct": 0, + "alert": "none", + "effect": "none", + "colormode": "hs", + "reachable": True + }, + "type": "Extended color light", + "name": "Hue Lamp 2", + "modelid": "LCT001", + "swversion": "66009461", + "manufacturername": "Philips", + "uniqueid": "123", +} +LIGHT_2_ON = { + "state": { + "on": True, + "bri": 100, + "hue": 13088, + "sat": 210, + "xy": [.5, .4], + "ct": 420, + "alert": "none", + "effect": "none", + "colormode": "hs", + "reachable": True + }, + "type": "Extended color light", + "name": "Hue Lamp 2 new", + "modelid": "LCT001", + "swversion": "66009461", + "manufacturername": "Philips", + "uniqueid": "123", +} +LIGHT_RESPONSE = { + "1": LIGHT_1_ON, + "2": LIGHT_2_OFF, +} -class TestHueLight(unittest.TestCase): - """Test the HueLight class.""" +@pytest.fixture +def mock_bridge(hass): + """Mock a Hue bridge.""" + bridge = Mock(available=True, allow_groups=False, host='1.1.1.1') + bridge.mock_requests = [] + # We're using a deque so we can schedule multiple responses + # and also means that `popleft()` will blow up if we get more updates + # than expected. + bridge.mock_light_responses = deque() + bridge.mock_group_responses = deque() - def setUp(self): # pylint: disable=invalid-name - """Setup things to be run when tests are started.""" - self.hass = get_test_home_assistant() - self.skip_teardown_stop = False + async def mock_request(method, path, **kwargs): + kwargs['method'] = method + kwargs['path'] = path + bridge.mock_requests.append(kwargs) - self.light_id = 42 - self.mock_info = MagicMock() - self.mock_bridge = MagicMock() - self.mock_update_lights = MagicMock() - self.mock_allow_unreachable = MagicMock() - self.mock_is_group = MagicMock() - self.mock_allow_in_emulated_hue = MagicMock() - self.mock_is_group = False + if path == 'lights': + return bridge.mock_light_responses.popleft() + elif path == 'groups': + return bridge.mock_group_responses.popleft() + return None - def tearDown(self): - """Stop everything that was started.""" - if not self.skip_teardown_stop: - self.hass.stop() + bridge.api.config.apiversion = '9.9.9' + bridge.api.lights = Lights({}, mock_request) + bridge.api.groups = Groups({}, mock_request) - def buildLight( - self, light_id=None, info=None, update_lights=None, is_group=None): - """Helper to build a HueLight object with minimal fuss.""" - if 'state' not in info: - on_key = 'any_on' if is_group is not None else 'on' - info['state'] = {on_key: False} + return bridge - return hue_light.HueLight( - light_id if light_id is not None else self.light_id, - info if info is not None else self.mock_info, - self.mock_bridge, - (update_lights - if update_lights is not None - else self.mock_update_lights), - self.mock_allow_unreachable, self.mock_allow_in_emulated_hue, - is_group if is_group is not None else self.mock_is_group) - def test_unique_id_for_light(self): - """Test the unique_id method with lights.""" - light = self.buildLight(info={'uniqueid': 'foobar'}) - self.assertEqual('foobar', light.unique_id) +async def setup_bridge(hass, mock_bridge): + """Load the Hue light platform with the provided bridge.""" + hass.config.components.add(hue.DOMAIN) + hass.data[hue.DOMAIN] = {'mock-host': mock_bridge} + await hass.helpers.discovery.async_load_platform('light', 'hue', { + 'host': 'mock-host' + }) + await hass.async_block_till_done() - light = self.buildLight(info={}) - self.assertIsNone(light.unique_id) - def test_unique_id_for_group(self): - """Test the unique_id method with groups.""" - light = self.buildLight(info={'uniqueid': 'foobar'}, is_group=True) - self.assertEqual('foobar', light.unique_id) +async def test_not_load_groups_if_old_bridge(hass, mock_bridge): + """Test that we don't try to load gorups if bridge runs old software.""" + mock_bridge.api.config.apiversion = '1.12.0' + mock_bridge.mock_light_responses.append({}) + mock_bridge.mock_group_responses.append(GROUP_RESPONSE) + await setup_bridge(hass, mock_bridge) + assert len(mock_bridge.mock_requests) == 1 + assert len(hass.states.async_all()) == 0 - light = self.buildLight(info={}, is_group=True) - self.assertIsNone(light.unique_id) + +async def test_no_lights_or_groups(hass, mock_bridge): + """Test the update_lights function when no lights are found.""" + mock_bridge.allow_groups = True + mock_bridge.mock_light_responses.append({}) + mock_bridge.mock_group_responses.append({}) + await setup_bridge(hass, mock_bridge) + assert len(mock_bridge.mock_requests) == 2 + assert len(hass.states.async_all()) == 0 + + +async def test_lights(hass, mock_bridge): + """Test the update_lights function with some lights.""" + mock_bridge.mock_light_responses.append(LIGHT_RESPONSE) + await setup_bridge(hass, mock_bridge) + assert len(mock_bridge.mock_requests) == 1 + # 1 All Lights group, 2 lights + assert len(hass.states.async_all()) == 3 + + lamp_1 = hass.states.get('light.hue_lamp_1') + assert lamp_1 is not None + assert lamp_1.state == 'on' + assert lamp_1.attributes['brightness'] == 144 + assert lamp_1.attributes['color_temp'] == 467 + + lamp_2 = hass.states.get('light.hue_lamp_2') + assert lamp_2 is not None + assert lamp_2.state == 'off' + + +async def test_groups(hass, mock_bridge): + """Test the update_lights function with some lights.""" + mock_bridge.allow_groups = True + mock_bridge.mock_light_responses.append({}) + mock_bridge.mock_group_responses.append(GROUP_RESPONSE) + + await setup_bridge(hass, mock_bridge) + assert len(mock_bridge.mock_requests) == 2 + # 1 all lights group, 2 hue group lights + assert len(hass.states.async_all()) == 3 + + lamp_1 = hass.states.get('light.group_1') + assert lamp_1 is not None + assert lamp_1.state == 'on' + assert lamp_1.attributes['brightness'] == 254 + assert lamp_1.attributes['color_temp'] == 250 + + lamp_2 = hass.states.get('light.group_2') + assert lamp_2 is not None + assert lamp_2.state == 'on' + + +async def test_new_group_discovered(hass, mock_bridge): + """Test if 2nd update has a new group.""" + mock_bridge.allow_groups = True + mock_bridge.mock_light_responses.append({}) + mock_bridge.mock_group_responses.append(GROUP_RESPONSE) + + await setup_bridge(hass, mock_bridge) + assert len(mock_bridge.mock_requests) == 2 + assert len(hass.states.async_all()) == 3 + + new_group_response = dict(GROUP_RESPONSE) + new_group_response['3'] = { + "name": "Group 3", + "lights": [ + "3", + "4", + "5" + ], + "type": "LightGroup", + "action": { + "on": True, + "bri": 153, + "hue": 4345, + "sat": 254, + "effect": "none", + "xy": [ + 0.5, + 0.5 + ], + "ct": 250, + "alert": "select", + "colormode": "ct" + }, + "state": { + "any_on": True, + "all_on": False, + } + } + + mock_bridge.mock_light_responses.append({}) + mock_bridge.mock_group_responses.append(new_group_response) + + # Calling a service will trigger the updates to run + await hass.services.async_call('light', 'turn_on', { + 'entity_id': 'light.group_1' + }, blocking=True) + # 2x group update, 2x light update, 1 turn on request + assert len(mock_bridge.mock_requests) == 5 + assert len(hass.states.async_all()) == 4 + + new_group = hass.states.get('light.group_3') + assert new_group is not None + assert new_group.state == 'on' + assert new_group.attributes['brightness'] == 153 + assert new_group.attributes['color_temp'] == 250 + + +async def test_new_light_discovered(hass, mock_bridge): + """Test if 2nd update has a new light.""" + mock_bridge.mock_light_responses.append(LIGHT_RESPONSE) + + await setup_bridge(hass, mock_bridge) + assert len(mock_bridge.mock_requests) == 1 + assert len(hass.states.async_all()) == 3 + + new_light_response = dict(LIGHT_RESPONSE) + new_light_response['3'] = { + "state": { + "on": False, + "bri": 0, + "hue": 0, + "sat": 0, + "xy": [0, 0], + "ct": 0, + "alert": "none", + "effect": "none", + "colormode": "hs", + "reachable": True + }, + "type": "Extended color light", + "name": "Hue Lamp 3", + "modelid": "LCT001", + "swversion": "66009461", + "manufacturername": "Philips", + "uniqueid": "789", + } + + mock_bridge.mock_light_responses.append(new_light_response) + + # Calling a service will trigger the updates to run + await hass.services.async_call('light', 'turn_on', { + 'entity_id': 'light.hue_lamp_1' + }, blocking=True) + # 2x light update, 1 turn on request + assert len(mock_bridge.mock_requests) == 3 + assert len(hass.states.async_all()) == 4 + + light = hass.states.get('light.hue_lamp_3') + assert light is not None + assert light.state == 'off' + + +async def test_other_group_update(hass, mock_bridge): + """Test changing one group that will impact the state of other light.""" + mock_bridge.allow_groups = True + mock_bridge.mock_light_responses.append({}) + mock_bridge.mock_group_responses.append(GROUP_RESPONSE) + + await setup_bridge(hass, mock_bridge) + assert len(mock_bridge.mock_requests) == 2 + assert len(hass.states.async_all()) == 3 + + group_2 = hass.states.get('light.group_2') + assert group_2 is not None + assert group_2.name == 'Group 2' + assert group_2.state == 'on' + assert group_2.attributes['brightness'] == 153 + assert group_2.attributes['color_temp'] == 250 + + updated_group_response = dict(GROUP_RESPONSE) + updated_group_response['2'] = { + "name": "Group 2 new", + "lights": [ + "3", + "4", + "5" + ], + "type": "LightGroup", + "action": { + "on": False, + "bri": 0, + "hue": 0, + "sat": 0, + "effect": "none", + "xy": [ + 0, + 0 + ], + "ct": 0, + "alert": "none", + "colormode": "ct" + }, + "state": { + "any_on": False, + "all_on": False, + } + } + + mock_bridge.mock_light_responses.append({}) + mock_bridge.mock_group_responses.append(updated_group_response) + + # Calling a service will trigger the updates to run + await hass.services.async_call('light', 'turn_on', { + 'entity_id': 'light.group_1' + }, blocking=True) + # 2x group update, 2x light update, 1 turn on request + assert len(mock_bridge.mock_requests) == 5 + assert len(hass.states.async_all()) == 3 + + group_2 = hass.states.get('light.group_2') + assert group_2 is not None + assert group_2.name == 'Group 2 new' + assert group_2.state == 'off' + + +async def test_other_light_update(hass, mock_bridge): + """Test changing one light that will impact state of other light.""" + mock_bridge.mock_light_responses.append(LIGHT_RESPONSE) + + await setup_bridge(hass, mock_bridge) + assert len(mock_bridge.mock_requests) == 1 + assert len(hass.states.async_all()) == 3 + + lamp_2 = hass.states.get('light.hue_lamp_2') + assert lamp_2 is not None + assert lamp_2.name == 'Hue Lamp 2' + assert lamp_2.state == 'off' + + updated_light_response = dict(LIGHT_RESPONSE) + updated_light_response['2'] = { + "state": { + "on": True, + "bri": 100, + "hue": 13088, + "sat": 210, + "xy": [.5, .4], + "ct": 420, + "alert": "none", + "effect": "none", + "colormode": "hs", + "reachable": True + }, + "type": "Extended color light", + "name": "Hue Lamp 2 new", + "modelid": "LCT001", + "swversion": "66009461", + "manufacturername": "Philips", + "uniqueid": "123", + } + + mock_bridge.mock_light_responses.append(updated_light_response) + + # Calling a service will trigger the updates to run + await hass.services.async_call('light', 'turn_on', { + 'entity_id': 'light.hue_lamp_1' + }, blocking=True) + # 2x light update, 1 turn on request + assert len(mock_bridge.mock_requests) == 3 + assert len(hass.states.async_all()) == 3 + + lamp_2 = hass.states.get('light.hue_lamp_2') + assert lamp_2 is not None + assert lamp_2.name == 'Hue Lamp 2 new' + assert lamp_2.state == 'on' + assert lamp_2.attributes['brightness'] == 100 + + +async def test_update_timeout(hass, mock_bridge): + """Test bridge marked as not available if timeout error during update.""" + mock_bridge.api.lights.update = Mock(side_effect=asyncio.TimeoutError) + mock_bridge.api.groups.update = Mock(side_effect=asyncio.TimeoutError) + await setup_bridge(hass, mock_bridge) + assert len(mock_bridge.mock_requests) == 0 + assert len(hass.states.async_all()) == 0 + assert mock_bridge.available is False + + +async def test_update_unauthorized(hass, mock_bridge): + """Test bridge marked as not available if unauthorized during update.""" + mock_bridge.api.lights.update = Mock(side_effect=aiohue.Unauthorized) + mock_bridge.api.groups.update = Mock(side_effect=aiohue.Unauthorized) + await setup_bridge(hass, mock_bridge) + assert len(mock_bridge.mock_requests) == 0 + assert len(hass.states.async_all()) == 0 + assert mock_bridge.available is False + + +async def test_light_turn_on_service(hass, mock_bridge): + """Test calling the turn on service on a light.""" + mock_bridge.mock_light_responses.append(LIGHT_RESPONSE) + await setup_bridge(hass, mock_bridge) + light = hass.states.get('light.hue_lamp_2') + assert light is not None + assert light.state == 'off' + + updated_light_response = dict(LIGHT_RESPONSE) + updated_light_response['2'] = LIGHT_2_ON + + mock_bridge.mock_light_responses.append(updated_light_response) + + await hass.services.async_call('light', 'turn_on', { + 'entity_id': 'light.hue_lamp_2', + 'brightness': 100, + 'color_temp': 300, + }, blocking=True) + # 2x light update, 1 turn on request + assert len(mock_bridge.mock_requests) == 3 + + assert mock_bridge.mock_requests[1]['json'] == { + 'bri': 100, + 'on': True, + 'ct': 300, + 'effect': 'none', + 'alert': 'none', + } + + assert len(hass.states.async_all()) == 3 + + light = hass.states.get('light.hue_lamp_2') + assert light is not None + assert light.state == 'on' + + +async def test_light_turn_off_service(hass, mock_bridge): + """Test calling the turn on service on a light.""" + mock_bridge.mock_light_responses.append(LIGHT_RESPONSE) + await setup_bridge(hass, mock_bridge) + light = hass.states.get('light.hue_lamp_1') + assert light is not None + assert light.state == 'on' + + updated_light_response = dict(LIGHT_RESPONSE) + updated_light_response['1'] = LIGHT_1_OFF + + mock_bridge.mock_light_responses.append(updated_light_response) + + await hass.services.async_call('light', 'turn_off', { + 'entity_id': 'light.hue_lamp_1', + }, blocking=True) + # 2x light update, 1 turn on request + assert len(mock_bridge.mock_requests) == 3 + + assert mock_bridge.mock_requests[1]['json'] == { + 'on': False, + 'alert': 'none', + } + + assert len(hass.states.async_all()) == 3 + + light = hass.states.get('light.hue_lamp_1') + assert light is not None + assert light.state == 'off' def test_available(): """Test available property.""" light = hue_light.HueLight( - info={'state': {'reachable': False}}, - allow_unreachable=False, + light=Mock(state={'reachable': False}), + request_bridge_update=None, + bridge=Mock(allow_unreachable=False), is_group=False, - - light_id=None, - bridge=mock.Mock(), - update_lights_cb=None, - allow_in_emulated_hue=False, ) assert light.available is False light = hue_light.HueLight( - info={'state': {'reachable': False}}, - allow_unreachable=True, + light=Mock(state={'reachable': False}), + request_bridge_update=None, + bridge=Mock(allow_unreachable=True), is_group=False, - - light_id=None, - bridge=mock.Mock(), - update_lights_cb=None, - allow_in_emulated_hue=False, ) assert light.available is True light = hue_light.HueLight( - info={'state': {'reachable': False}}, - allow_unreachable=False, + light=Mock(state={'reachable': False}), + request_bridge_update=None, + bridge=Mock(allow_unreachable=False), is_group=True, - - light_id=None, - bridge=mock.Mock(), - update_lights_cb=None, - allow_in_emulated_hue=False, ) assert light.available is True diff --git a/tests/components/test_hue.py b/tests/components/test_hue.py deleted file mode 100644 index 78f8b573666..00000000000 --- a/tests/components/test_hue.py +++ /dev/null @@ -1,588 +0,0 @@ -"""Generic Philips Hue component tests.""" -import asyncio -import logging -import unittest -from unittest.mock import call, MagicMock, patch - -import aiohue -import pytest -import voluptuous as vol - -from homeassistant.components import configurator, hue -from homeassistant.const import CONF_FILENAME, CONF_HOST -from homeassistant.setup import setup_component, async_setup_component - -from tests.common import ( - assert_setup_component, get_test_home_assistant, get_test_config_dir, - MockDependency, MockConfigEntry, mock_coro -) - -_LOGGER = logging.getLogger(__name__) - - -class TestSetup(unittest.TestCase): - """Test the Hue component.""" - - def setUp(self): # pylint: disable=invalid-name - """Setup things to be run when tests are started.""" - self.hass = get_test_home_assistant() - self.skip_teardown_stop = False - - def tearDown(self): - """Stop everything that was started.""" - if not self.skip_teardown_stop: - self.hass.stop() - - @MockDependency('phue') - def test_setup_no_domain(self, mock_phue): - """If it's not in the config we won't even try.""" - with assert_setup_component(0): - self.assertTrue(setup_component( - self.hass, hue.DOMAIN, {})) - mock_phue.Bridge.assert_not_called() - self.assertEqual({}, self.hass.data[hue.DOMAIN]) - - @MockDependency('phue') - def test_setup_with_host(self, mock_phue): - """Host specified in the config file.""" - mock_bridge = mock_phue.Bridge - - with assert_setup_component(1): - with patch('homeassistant.helpers.discovery.load_platform') \ - as mock_load: - self.assertTrue(setup_component( - self.hass, hue.DOMAIN, - {hue.DOMAIN: {hue.CONF_BRIDGES: [ - {CONF_HOST: 'localhost'}]}})) - - mock_bridge.assert_called_once_with( - 'localhost', - config_file_path=get_test_config_dir(hue.PHUE_CONFIG_FILE)) - mock_load.assert_called_once_with( - self.hass, 'light', hue.DOMAIN, - {'bridge_id': '127.0.0.1'}) - - self.assertTrue(hue.DOMAIN in self.hass.data) - self.assertEqual(1, len(self.hass.data[hue.DOMAIN])) - - @MockDependency('phue') - def test_setup_with_phue_conf(self, mock_phue): - """No host in the config file, but one is cached in phue.conf.""" - mock_bridge = mock_phue.Bridge - - with assert_setup_component(1): - with patch( - 'homeassistant.components.hue._find_host_from_config', - return_value='localhost'): - with patch('homeassistant.helpers.discovery.load_platform') \ - as mock_load: - self.assertTrue(setup_component( - self.hass, hue.DOMAIN, - {hue.DOMAIN: {hue.CONF_BRIDGES: [ - {CONF_FILENAME: 'phue.conf'}]}})) - - mock_bridge.assert_called_once_with( - 'localhost', - config_file_path=get_test_config_dir( - hue.PHUE_CONFIG_FILE)) - mock_load.assert_called_once_with( - self.hass, 'light', hue.DOMAIN, - {'bridge_id': '127.0.0.1'}) - - self.assertTrue(hue.DOMAIN in self.hass.data) - self.assertEqual(1, len(self.hass.data[hue.DOMAIN])) - - @MockDependency('phue') - def test_setup_with_multiple_hosts(self, mock_phue): - """Multiple hosts specified in the config file.""" - mock_bridge = mock_phue.Bridge - - with assert_setup_component(1): - with patch('homeassistant.helpers.discovery.load_platform') \ - as mock_load: - self.assertTrue(setup_component( - self.hass, hue.DOMAIN, - {hue.DOMAIN: {hue.CONF_BRIDGES: [ - {CONF_HOST: 'localhost'}, - {CONF_HOST: '192.168.0.1'}]}})) - - mock_bridge.assert_has_calls([ - call( - 'localhost', - config_file_path=get_test_config_dir( - hue.PHUE_CONFIG_FILE)), - call( - '192.168.0.1', - config_file_path=get_test_config_dir( - hue.PHUE_CONFIG_FILE))]) - mock_load.mock_bridge.assert_not_called() - mock_load.assert_has_calls([ - call( - self.hass, 'light', hue.DOMAIN, - {'bridge_id': '127.0.0.1'}), - call( - self.hass, 'light', hue.DOMAIN, - {'bridge_id': '192.168.0.1'}), - ], any_order=True) - - self.assertTrue(hue.DOMAIN in self.hass.data) - self.assertEqual(2, len(self.hass.data[hue.DOMAIN])) - - @MockDependency('phue') - def test_bridge_discovered(self, mock_phue): - """Bridge discovery.""" - mock_bridge = mock_phue.Bridge - mock_service = MagicMock() - discovery_info = {'host': '192.168.0.10', 'serial': 'foobar'} - - with patch('homeassistant.helpers.discovery.load_platform') \ - as mock_load: - self.assertTrue(setup_component( - self.hass, hue.DOMAIN, {})) - hue.bridge_discovered(self.hass, mock_service, discovery_info) - - mock_bridge.assert_called_once_with( - '192.168.0.10', - config_file_path=get_test_config_dir('phue-foobar.conf')) - mock_load.assert_called_once_with( - self.hass, 'light', hue.DOMAIN, - {'bridge_id': '192.168.0.10'}) - - self.assertTrue(hue.DOMAIN in self.hass.data) - self.assertEqual(1, len(self.hass.data[hue.DOMAIN])) - - @MockDependency('phue') - def test_bridge_configure_and_discovered(self, mock_phue): - """Bridge is in the config file, then we discover it.""" - mock_bridge = mock_phue.Bridge - mock_service = MagicMock() - discovery_info = {'host': '192.168.1.10', 'serial': 'foobar'} - - with assert_setup_component(1): - with patch('homeassistant.helpers.discovery.load_platform') \ - as mock_load: - # First we set up the component from config - self.assertTrue(setup_component( - self.hass, hue.DOMAIN, - {hue.DOMAIN: {hue.CONF_BRIDGES: [ - {CONF_HOST: '192.168.1.10'}]}})) - - mock_bridge.assert_called_once_with( - '192.168.1.10', - config_file_path=get_test_config_dir( - hue.PHUE_CONFIG_FILE)) - calls_to_mock_load = [ - call( - self.hass, 'light', hue.DOMAIN, - {'bridge_id': '192.168.1.10'}), - ] - mock_load.assert_has_calls(calls_to_mock_load) - - self.assertTrue(hue.DOMAIN in self.hass.data) - self.assertEqual(1, len(self.hass.data[hue.DOMAIN])) - - # Then we discover the same bridge - hue.bridge_discovered(self.hass, mock_service, discovery_info) - - # No additional calls - mock_bridge.assert_called_once_with( - '192.168.1.10', - config_file_path=get_test_config_dir( - hue.PHUE_CONFIG_FILE)) - mock_load.assert_has_calls(calls_to_mock_load) - - # Still only one - self.assertTrue(hue.DOMAIN in self.hass.data) - self.assertEqual(1, len(self.hass.data[hue.DOMAIN])) - - -class TestHueBridge(unittest.TestCase): - """Test the HueBridge class.""" - - def setUp(self): # pylint: disable=invalid-name - """Setup things to be run when tests are started.""" - self.hass = get_test_home_assistant() - self.hass.data[hue.DOMAIN] = {} - self.skip_teardown_stop = False - - def tearDown(self): - """Stop everything that was started.""" - if not self.skip_teardown_stop: - self.hass.stop() - - @MockDependency('phue') - def test_setup_bridge_connection_refused(self, mock_phue): - """Test a registration failed with a connection refused exception.""" - mock_bridge = mock_phue.Bridge - mock_bridge.side_effect = ConnectionRefusedError() - - bridge = hue.HueBridge( - 'localhost', self.hass, hue.PHUE_CONFIG_FILE, None) - bridge.setup() - self.assertFalse(bridge.configured) - self.assertTrue(bridge.config_request_id is None) - - mock_bridge.assert_called_once_with( - 'localhost', - config_file_path=get_test_config_dir(hue.PHUE_CONFIG_FILE)) - - @MockDependency('phue') - def test_setup_bridge_registration_exception(self, mock_phue): - """Test a registration failed with an exception.""" - mock_bridge = mock_phue.Bridge - mock_phue.PhueRegistrationException = Exception - mock_bridge.side_effect = mock_phue.PhueRegistrationException(1, 2) - - bridge = hue.HueBridge( - 'localhost', self.hass, hue.PHUE_CONFIG_FILE, None) - bridge.setup() - self.assertFalse(bridge.configured) - self.assertFalse(bridge.config_request_id is None) - self.assertTrue(isinstance(bridge.config_request_id, str)) - - mock_bridge.assert_called_once_with( - 'localhost', - config_file_path=get_test_config_dir(hue.PHUE_CONFIG_FILE)) - - @MockDependency('phue') - def test_setup_bridge_registration_succeeds(self, mock_phue): - """Test a registration success sequence.""" - mock_bridge = mock_phue.Bridge - mock_phue.PhueRegistrationException = Exception - mock_bridge.side_effect = [ - # First call, raise because not registered - mock_phue.PhueRegistrationException(1, 2), - # Second call, registration is done - None, - ] - - bridge = hue.HueBridge( - 'localhost', self.hass, hue.PHUE_CONFIG_FILE, None) - bridge.setup() - self.assertFalse(bridge.configured) - self.assertFalse(bridge.config_request_id is None) - - # Simulate the user confirming the registration - self.hass.services.call( - configurator.DOMAIN, configurator.SERVICE_CONFIGURE, - {configurator.ATTR_CONFIGURE_ID: bridge.config_request_id}) - - self.hass.block_till_done() - self.assertTrue(bridge.configured) - self.assertTrue(bridge.config_request_id is None) - - # We should see a total of two identical calls - args = call( - 'localhost', - config_file_path=get_test_config_dir(hue.PHUE_CONFIG_FILE)) - mock_bridge.assert_has_calls([args, args]) - - # Make sure the request is done - self.assertEqual(1, len(self.hass.states.all())) - self.assertEqual('configured', self.hass.states.all()[0].state) - - @MockDependency('phue') - def test_setup_bridge_registration_fails(self, mock_phue): - """ - Test a registration failure sequence. - - This may happen when we start the registration process, the user - responds to the request but the bridge has become unreachable. - """ - mock_bridge = mock_phue.Bridge - mock_phue.PhueRegistrationException = Exception - mock_bridge.side_effect = [ - # First call, raise because not registered - mock_phue.PhueRegistrationException(1, 2), - # Second call, the bridge has gone away - ConnectionRefusedError(), - ] - - bridge = hue.HueBridge( - 'localhost', self.hass, hue.PHUE_CONFIG_FILE, None) - bridge.setup() - self.assertFalse(bridge.configured) - self.assertFalse(bridge.config_request_id is None) - - # Simulate the user confirming the registration - self.hass.services.call( - configurator.DOMAIN, configurator.SERVICE_CONFIGURE, - {configurator.ATTR_CONFIGURE_ID: bridge.config_request_id}) - - self.hass.block_till_done() - self.assertFalse(bridge.configured) - self.assertFalse(bridge.config_request_id is None) - - # We should see a total of two identical calls - args = call( - 'localhost', - config_file_path=get_test_config_dir(hue.PHUE_CONFIG_FILE)) - mock_bridge.assert_has_calls([args, args]) - - # The request should still be pending - self.assertEqual(1, len(self.hass.states.all())) - self.assertEqual('configure', self.hass.states.all()[0].state) - - @MockDependency('phue') - def test_setup_bridge_registration_retry(self, mock_phue): - """ - Test a registration retry sequence. - - This may happen when we start the registration process, the user - responds to the request but we fail to confirm it with the bridge. - """ - mock_bridge = mock_phue.Bridge - mock_phue.PhueRegistrationException = Exception - mock_bridge.side_effect = [ - # First call, raise because not registered - mock_phue.PhueRegistrationException(1, 2), - # Second call, for whatever reason authentication fails - mock_phue.PhueRegistrationException(1, 2), - ] - - bridge = hue.HueBridge( - 'localhost', self.hass, hue.PHUE_CONFIG_FILE, None) - bridge.setup() - self.assertFalse(bridge.configured) - self.assertFalse(bridge.config_request_id is None) - - # Simulate the user confirming the registration - self.hass.services.call( - configurator.DOMAIN, configurator.SERVICE_CONFIGURE, - {configurator.ATTR_CONFIGURE_ID: bridge.config_request_id}) - - self.hass.block_till_done() - self.assertFalse(bridge.configured) - self.assertFalse(bridge.config_request_id is None) - - # We should see a total of two identical calls - args = call( - 'localhost', - config_file_path=get_test_config_dir(hue.PHUE_CONFIG_FILE)) - mock_bridge.assert_has_calls([args, args]) - - # Make sure the request is done - self.assertEqual(1, len(self.hass.states.all())) - self.assertEqual('configure', self.hass.states.all()[0].state) - self.assertEqual( - 'Failed to register, please try again.', - self.hass.states.all()[0].attributes.get(configurator.ATTR_ERRORS)) - - @MockDependency('phue') - def test_hue_activate_scene(self, mock_phue): - """Test the hue_activate_scene service.""" - with patch('homeassistant.helpers.discovery.load_platform'): - bridge = hue.HueBridge('localhost', self.hass, - hue.PHUE_CONFIG_FILE, None) - bridge.setup() - - # No args - self.hass.services.call(hue.DOMAIN, hue.SERVICE_HUE_SCENE, - blocking=True) - bridge.bridge.run_scene.assert_not_called() - - # Only one arg - self.hass.services.call( - hue.DOMAIN, hue.SERVICE_HUE_SCENE, - {hue.ATTR_GROUP_NAME: 'group'}, - blocking=True) - bridge.bridge.run_scene.assert_not_called() - - self.hass.services.call( - hue.DOMAIN, hue.SERVICE_HUE_SCENE, - {hue.ATTR_SCENE_NAME: 'scene'}, - blocking=True) - bridge.bridge.run_scene.assert_not_called() - - # Both required args - self.hass.services.call( - hue.DOMAIN, hue.SERVICE_HUE_SCENE, - {hue.ATTR_GROUP_NAME: 'group', hue.ATTR_SCENE_NAME: 'scene'}, - blocking=True) - bridge.bridge.run_scene.assert_called_once_with('group', 'scene') - - -async def test_setup_no_host(hass, requests_mock): - """No host specified in any way.""" - requests_mock.get(hue.API_NUPNP, json=[]) - with MockDependency('phue') as mock_phue: - result = await async_setup_component( - hass, hue.DOMAIN, {hue.DOMAIN: {}}) - assert result - - mock_phue.Bridge.assert_not_called() - - assert hass.data[hue.DOMAIN] == {} - - -async def test_flow_works(hass, aioclient_mock): - """Test config flow .""" - aioclient_mock.get(hue.API_NUPNP, json=[ - {'internalipaddress': '1.2.3.4', 'id': 'bla'} - ]) - - flow = hue.HueFlowHandler() - flow.hass = hass - await flow.async_step_init() - - with patch('aiohue.Bridge') as mock_bridge: - def mock_constructor(host, websession): - mock_bridge.host = host - return mock_bridge - - mock_bridge.side_effect = mock_constructor - mock_bridge.username = 'username-abc' - mock_bridge.config.name = 'Mock Bridge' - mock_bridge.config.bridgeid = 'bridge-id-1234' - mock_bridge.create_user.return_value = mock_coro() - mock_bridge.initialize.return_value = mock_coro() - - result = await flow.async_step_link(user_input={}) - - assert mock_bridge.host == '1.2.3.4' - assert len(mock_bridge.create_user.mock_calls) == 1 - assert len(mock_bridge.initialize.mock_calls) == 1 - - assert result['type'] == 'create_entry' - assert result['title'] == 'Mock Bridge' - assert result['data'] == { - 'host': '1.2.3.4', - 'bridge_id': 'bridge-id-1234', - 'username': 'username-abc' - } - - -async def test_flow_no_discovered_bridges(hass, aioclient_mock): - """Test config flow discovers no bridges.""" - aioclient_mock.get(hue.API_NUPNP, json=[]) - flow = hue.HueFlowHandler() - flow.hass = hass - - result = await flow.async_step_init() - assert result['type'] == 'abort' - - -async def test_flow_all_discovered_bridges_exist(hass, aioclient_mock): - """Test config flow discovers only already configured bridges.""" - aioclient_mock.get(hue.API_NUPNP, json=[ - {'internalipaddress': '1.2.3.4', 'id': 'bla'} - ]) - MockConfigEntry(domain='hue', data={ - 'host': '1.2.3.4' - }).add_to_hass(hass) - flow = hue.HueFlowHandler() - flow.hass = hass - - result = await flow.async_step_init() - assert result['type'] == 'abort' - - -async def test_flow_one_bridge_discovered(hass, aioclient_mock): - """Test config flow discovers one bridge.""" - aioclient_mock.get(hue.API_NUPNP, json=[ - {'internalipaddress': '1.2.3.4', 'id': 'bla'} - ]) - flow = hue.HueFlowHandler() - flow.hass = hass - - result = await flow.async_step_init() - assert result['type'] == 'form' - assert result['step_id'] == 'link' - - -async def test_flow_two_bridges_discovered(hass, aioclient_mock): - """Test config flow discovers two bridges.""" - aioclient_mock.get(hue.API_NUPNP, json=[ - {'internalipaddress': '1.2.3.4', 'id': 'bla'}, - {'internalipaddress': '5.6.7.8', 'id': 'beer'} - ]) - flow = hue.HueFlowHandler() - flow.hass = hass - - result = await flow.async_step_init() - assert result['type'] == 'form' - assert result['step_id'] == 'init' - - with pytest.raises(vol.Invalid): - assert result['data_schema']({'host': '0.0.0.0'}) - - result['data_schema']({'host': '1.2.3.4'}) - result['data_schema']({'host': '5.6.7.8'}) - - -async def test_flow_two_bridges_discovered_one_new(hass, aioclient_mock): - """Test config flow discovers two bridges.""" - aioclient_mock.get(hue.API_NUPNP, json=[ - {'internalipaddress': '1.2.3.4', 'id': 'bla'}, - {'internalipaddress': '5.6.7.8', 'id': 'beer'} - ]) - MockConfigEntry(domain='hue', data={ - 'host': '1.2.3.4' - }).add_to_hass(hass) - flow = hue.HueFlowHandler() - flow.hass = hass - - result = await flow.async_step_init() - assert result['type'] == 'form' - assert result['step_id'] == 'link' - assert flow.host == '5.6.7.8' - - -async def test_flow_timeout_discovery(hass): - """Test config flow .""" - flow = hue.HueFlowHandler() - flow.hass = hass - - with patch('aiohue.discovery.discover_nupnp', - side_effect=asyncio.TimeoutError): - result = await flow.async_step_init() - - assert result['type'] == 'abort' - - -async def test_flow_link_timeout(hass): - """Test config flow .""" - flow = hue.HueFlowHandler() - flow.hass = hass - - with patch('aiohue.Bridge.create_user', - side_effect=asyncio.TimeoutError): - result = await flow.async_step_link({}) - - assert result['type'] == 'form' - assert result['step_id'] == 'link' - assert result['errors'] == { - 'base': 'register_failed' - } - - -async def test_flow_link_button_not_pressed(hass): - """Test config flow .""" - flow = hue.HueFlowHandler() - flow.hass = hass - - with patch('aiohue.Bridge.create_user', - side_effect=aiohue.LinkButtonNotPressed): - result = await flow.async_step_link({}) - - assert result['type'] == 'form' - assert result['step_id'] == 'link' - assert result['errors'] == { - 'base': 'register_failed' - } - - -async def test_flow_link_unknown_host(hass): - """Test config flow .""" - flow = hue.HueFlowHandler() - flow.hass = hass - - with patch('aiohue.Bridge.create_user', - side_effect=aiohue.RequestError): - result = await flow.async_step_link({}) - - assert result['type'] == 'form' - assert result['step_id'] == 'link' - assert result['errors'] == { - 'base': 'register_failed' - } diff --git a/tests/components/zwave/test_init.py b/tests/components/zwave/test_init.py index cdbf91d09e5..30c9d3ba489 100644 --- a/tests/components/zwave/test_init.py +++ b/tests/components/zwave/test_init.py @@ -17,7 +17,7 @@ from homeassistant.setup import setup_component import pytest from tests.common import ( - get_test_home_assistant, async_fire_time_changed) + get_test_home_assistant, async_fire_time_changed, mock_coro) from tests.mock.zwave import MockNetwork, MockNode, MockValue, MockEntityValues @@ -468,6 +468,7 @@ class TestZWaveDeviceEntityValues(unittest.TestCase): @patch.object(zwave, 'discovery') def test_entity_discovery(self, discovery, get_platform): """Test the creation of a new entity.""" + discovery.async_load_platform.return_value = mock_coro() mock_platform = MagicMock() get_platform.return_value = mock_platform mock_device = MagicMock() @@ -500,8 +501,7 @@ class TestZWaveDeviceEntityValues(unittest.TestCase): key=lambda a: id(a))) assert discovery.async_load_platform.called - # Second call is to async yield from - assert len(discovery.async_load_platform.mock_calls) == 2 + assert len(discovery.async_load_platform.mock_calls) == 1 args = discovery.async_load_platform.mock_calls[0][1] assert args[0] == self.hass assert args[1] == 'mock_component' @@ -532,6 +532,7 @@ class TestZWaveDeviceEntityValues(unittest.TestCase): @patch.object(zwave, 'discovery') def test_entity_existing_values(self, discovery, get_platform): """Test the loading of already discovered values.""" + discovery.async_load_platform.return_value = mock_coro() mock_platform = MagicMock() get_platform.return_value = mock_platform mock_device = MagicMock() @@ -563,8 +564,7 @@ class TestZWaveDeviceEntityValues(unittest.TestCase): key=lambda a: id(a))) assert discovery.async_load_platform.called - # Second call is to async yield from - assert len(discovery.async_load_platform.mock_calls) == 2 + assert len(discovery.async_load_platform.mock_calls) == 1 args = discovery.async_load_platform.mock_calls[0][1] assert args[0] == self.hass assert args[1] == 'mock_component' @@ -599,6 +599,7 @@ class TestZWaveDeviceEntityValues(unittest.TestCase): @patch.object(zwave, 'discovery') def test_entity_workaround_component(self, discovery, get_platform): """Test ignore workaround.""" + discovery.async_load_platform.return_value = mock_coro() mock_platform = MagicMock() get_platform.return_value = mock_platform mock_device = MagicMock() @@ -629,8 +630,7 @@ class TestZWaveDeviceEntityValues(unittest.TestCase): self.hass.block_till_done() assert discovery.async_load_platform.called - # Second call is to async yield from - assert len(discovery.async_load_platform.mock_calls) == 2 + assert len(discovery.async_load_platform.mock_calls) == 1 args = discovery.async_load_platform.mock_calls[0][1] assert args[1] == 'binary_sensor' diff --git a/tests/helpers/test_discovery.py b/tests/helpers/test_discovery.py index 2087dc2adb5..b345400ba17 100644 --- a/tests/helpers/test_discovery.py +++ b/tests/helpers/test_discovery.py @@ -1,5 +1,4 @@ """Test discovery helpers.""" -import asyncio from unittest.mock import patch import pytest @@ -24,7 +23,8 @@ class TestHelpersDiscovery: """Stop everything that was started.""" self.hass.stop() - @patch('homeassistant.setup.async_setup_component') + @patch('homeassistant.setup.async_setup_component', + return_value=mock_coro()) def test_listen(self, mock_setup_component): """Test discovery listen/discover combo.""" helpers = self.hass.helpers @@ -199,15 +199,13 @@ class TestHelpersDiscovery: assert len(component_calls) == 1 -@asyncio.coroutine -def test_load_platform_forbids_config(): +async def test_load_platform_forbids_config(): """Test you cannot setup config component with load_platform.""" with pytest.raises(HomeAssistantError): - yield from discovery.async_load_platform(None, 'config', 'zwave') + await discovery.async_load_platform(None, 'config', 'zwave') -@asyncio.coroutine -def test_discover_forbids_config(): +async def test_discover_forbids_config(): """Test you cannot setup config component with load_platform.""" with pytest.raises(HomeAssistantError): - yield from discovery.async_discover(None, None, None, 'config') + await discovery.async_discover(None, None, None, 'config') diff --git a/tests/util/test_init.py b/tests/util/test_init.py index 5493843c246..60b0e68ca59 100644 --- a/tests/util/test_init.py +++ b/tests/util/test_init.py @@ -291,3 +291,11 @@ async def test_throttle_async(): assert (await test_method()) is True assert (await test_method()) is None + + @util.Throttle(timedelta(seconds=2), timedelta(seconds=0.1)) + async def test_method2(): + """Only first call should return a value.""" + return True + + assert (await test_method2()) is True + assert (await test_method2()) is None From 66c6f9cdd66517cb49cd0e30c38c46d09f7d2d35 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Sat, 17 Mar 2018 11:51:40 +0100 Subject: [PATCH 121/220] Unused xiaomi miio sensor method removed (#13281) * Unused method removed. * remove unused import --- homeassistant/components/sensor/xiaomi_miio.py | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/homeassistant/components/sensor/xiaomi_miio.py b/homeassistant/components/sensor/xiaomi_miio.py index af7534d9112..cb172735ac4 100644 --- a/homeassistant/components/sensor/xiaomi_miio.py +++ b/homeassistant/components/sensor/xiaomi_miio.py @@ -4,7 +4,6 @@ Support for Xiaomi Mi Air Quality Monitor (PM2.5). For more details about this platform, please refer to the documentation https://home-assistant.io/components/sensor.xiaomi_miio/ """ -from functools import partial import logging import voluptuous as vol @@ -131,21 +130,6 @@ class XiaomiAirQualityMonitor(Entity): """Return the state attributes of the device.""" return self._state_attrs - async def _try_command(self, mask_error, func, *args, **kwargs): - """Call a device command handling error messages.""" - from miio import DeviceException - try: - result = await self.hass.async_add_job( - partial(func, *args, **kwargs)) - - _LOGGER.debug("Response received from miio device: %s", result) - - return result == SUCCESS - except DeviceException as exc: - _LOGGER.error(mask_error, exc) - self._available = False - return False - async def async_update(self): """Fetch state from the miio device.""" from miio import DeviceException From 05676ba18b02746d0b49befd20ef1088180e77de Mon Sep 17 00:00:00 2001 From: thrawnarn Date: Sat, 17 Mar 2018 12:14:01 +0100 Subject: [PATCH 122/220] Changed to async/await (#13246) * Changed to async/await * Hound fixes * Lint fixes * Changed sleep --- .../components/media_player/bluesound.py | 132 +++++++----------- 1 file changed, 52 insertions(+), 80 deletions(-) diff --git a/homeassistant/components/media_player/bluesound.py b/homeassistant/components/media_player/bluesound.py index a07e577c969..1b6310d4cab 100644 --- a/homeassistant/components/media_player/bluesound.py +++ b/homeassistant/components/media_player/bluesound.py @@ -131,8 +131,8 @@ def _add_player(hass, async_add_devices, host, port=None, name=None): hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, _init_player) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_devices, discovery_info=None): +async def async_setup_platform(hass, config, async_add_devices, + discovery_info=None): """Set up the Bluesound platforms.""" if DATA_BLUESOUND not in hass.data: hass.data[DATA_BLUESOUND] = [] @@ -149,8 +149,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): hass, async_add_devices, host.get(CONF_HOST), host.get(CONF_PORT), host.get(CONF_NAME)) - @asyncio.coroutine - def async_service_handler(service): + async def async_service_handler(service): """Map services to method of Bluesound devices.""" method = SERVICE_TO_METHOD.get(service.service) if not method: @@ -166,7 +165,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): target_players = hass.data[DATA_BLUESOUND] for player in target_players: - yield from getattr(player, method['method'])(**params) + await getattr(player, method['method'])(**params) for service in SERVICE_TO_METHOD: schema = SERVICE_TO_METHOD[service]['schema'] @@ -211,13 +210,12 @@ class BluesoundPlayer(MediaPlayerDevice): except ValueError: return -1 - @asyncio.coroutine - def force_update_sync_status( + async def force_update_sync_status( self, on_updated_cb=None, raise_timeout=False): """Update the internal status.""" resp = None try: - resp = yield from self.send_bluesound_command( + resp = await self.send_bluesound_command( 'SyncStatus', raise_timeout, raise_timeout) except Exception: raise @@ -254,16 +252,15 @@ class BluesoundPlayer(MediaPlayerDevice): on_updated_cb() return True - @asyncio.coroutine - def _start_poll_command(self): + async def _start_poll_command(self): """Loop which polls the status of the player.""" try: while True: - yield from self.async_update_status() + await self.async_update_status() except (asyncio.TimeoutError, ClientError): _LOGGER.info("Node %s is offline, retrying later", self._name) - yield from asyncio.sleep( + await asyncio.sleep( NODE_OFFLINE_CHECK_TIMEOUT, loop=self._hass.loop) self.start_polling() @@ -282,15 +279,14 @@ class BluesoundPlayer(MediaPlayerDevice): """Stop the polling task.""" self._polling_task.cancel() - @asyncio.coroutine - def async_init(self): + async def async_init(self, triggered=None): """Initialize the player async.""" try: if self._retry_remove is not None: self._retry_remove() self._retry_remove = None - yield from self.force_update_sync_status( + await self.force_update_sync_status( self._init_callback, True) except (asyncio.TimeoutError, ClientError): _LOGGER.info("Node %s is offline, retrying later", self.host) @@ -301,20 +297,18 @@ class BluesoundPlayer(MediaPlayerDevice): self.host) raise - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Update internal status of the entity.""" if not self._is_online: return - yield from self.async_update_sync_status() - yield from self.async_update_presets() - yield from self.async_update_captures() - yield from self.async_update_services() + await self.async_update_sync_status() + await self.async_update_presets() + await self.async_update_captures() + await self.async_update_services() - @asyncio.coroutine - def send_bluesound_command(self, method, raise_timeout=False, - allow_offline=False): + async def send_bluesound_command(self, method, raise_timeout=False, + allow_offline=False): """Send command to the player.""" import xmltodict @@ -330,10 +324,10 @@ class BluesoundPlayer(MediaPlayerDevice): try: websession = async_get_clientsession(self._hass) with async_timeout.timeout(10, loop=self._hass.loop): - response = yield from websession.get(url) + response = await websession.get(url) if response.status == 200: - result = yield from response.text() + result = await response.text() if len(result) < 1: data = None else: @@ -352,8 +346,7 @@ class BluesoundPlayer(MediaPlayerDevice): return data - @asyncio.coroutine - def async_update_status(self): + async def async_update_status(self): """Use the poll session to always get the status of the player.""" import xmltodict response = None @@ -372,7 +365,7 @@ class BluesoundPlayer(MediaPlayerDevice): try: with async_timeout.timeout(125, loop=self._hass.loop): - response = yield from self._polling_session.get( + response = await self._polling_session.get( url, headers={CONNECTION: KEEP_ALIVE}) @@ -380,7 +373,7 @@ class BluesoundPlayer(MediaPlayerDevice): _LOGGER.error("Error %s on %s. Trying one more time.", response.status, url) else: - result = yield from response.text() + result = await response.text() self._is_online = True self._last_status_update = dt_util.utcnow() self._status = xmltodict.parse(result)['status'].copy() @@ -392,8 +385,8 @@ class BluesoundPlayer(MediaPlayerDevice): self._group_name = group_name # the sleep is needed to make sure that the # devices is synced - yield from asyncio.sleep(1, loop=self._hass.loop) - yield from self.async_trigger_sync_on_all() + await asyncio.sleep(1, loop=self._hass.loop) + await self.async_trigger_sync_on_all() elif self.is_grouped: # when player is grouped we need to fetch volume from # sync_status. We will force an update if the player is @@ -402,7 +395,7 @@ class BluesoundPlayer(MediaPlayerDevice): # the device is playing. This would solve alot of # problems. This change will be done when the # communication is moved to a separate library - yield from self.force_update_sync_status() + await self.force_update_sync_status() self.async_schedule_update_ha_state() @@ -415,13 +408,12 @@ class BluesoundPlayer(MediaPlayerDevice): self._name) raise - @asyncio.coroutine - def async_trigger_sync_on_all(self): + async def async_trigger_sync_on_all(self): """Trigger sync status update on all devices.""" _LOGGER.debug("Trigger sync status on all devices") for player in self._hass.data[DATA_BLUESOUND]: - yield from player.force_update_sync_status() + await player.force_update_sync_status() @Throttle(SYNC_STATUS_INTERVAL) async def async_update_sync_status(self, on_updated_cb=None, @@ -788,8 +780,7 @@ class BluesoundPlayer(MediaPlayerDevice): """Return true if shuffle is active.""" return True if self._status.get('shuffle', '0') == '1' else False - @asyncio.coroutine - def async_join(self, master): + async def async_join(self, master): """Join the player to a group.""" master_device = [device for device in self.hass.data[DATA_BLUESOUND] if device.entity_id == master] @@ -798,37 +789,33 @@ class BluesoundPlayer(MediaPlayerDevice): _LOGGER.debug("Trying to join player: %s to master: %s", self.host, master_device[0].host) - yield from master_device[0].async_add_slave(self) + await master_device[0].async_add_slave(self) else: _LOGGER.error("Master not found %s", master_device) - @asyncio.coroutine - def async_unjoin(self): + async def async_unjoin(self): """Unjoin the player from a group.""" if self._master is None: return _LOGGER.debug("Trying to unjoin player: %s", self.host) - yield from self._master.async_remove_slave(self) + await self._master.async_remove_slave(self) - @asyncio.coroutine - def async_add_slave(self, slave_device): + async def async_add_slave(self, slave_device): """Add slave to master.""" return self.send_bluesound_command('/AddSlave?slave={}&port={}' .format(slave_device.host, slave_device.port)) - @asyncio.coroutine - def async_remove_slave(self, slave_device): + async def async_remove_slave(self, slave_device): """Remove slave to master.""" return self.send_bluesound_command('/RemoveSlave?slave={}&port={}' .format(slave_device.host, slave_device.port)) - @asyncio.coroutine - def async_increase_timer(self): + async def async_increase_timer(self): """Increase sleep time on player.""" - sleep_time = yield from self.send_bluesound_command('/Sleep') + sleep_time = await self.send_bluesound_command('/Sleep') if sleep_time is None: _LOGGER.error('Error while increasing sleep time on player: %s', self.host) @@ -836,21 +823,18 @@ class BluesoundPlayer(MediaPlayerDevice): return int(sleep_time.get('sleep', '0')) - @asyncio.coroutine - def async_clear_timer(self): + async def async_clear_timer(self): """Clear sleep timer on player.""" sleep = 1 while sleep > 0: - sleep = yield from self.async_increase_timer() + sleep = await self.async_increase_timer() - @asyncio.coroutine - def async_set_shuffle(self, shuffle): + async def async_set_shuffle(self, shuffle): """Enable or disable shuffle mode.""" return self.send_bluesound_command('/Shuffle?state={}' .format('1' if shuffle else '0')) - @asyncio.coroutine - def async_select_source(self, source): + async def async_select_source(self, source): """Select input source.""" if self.is_grouped and not self.is_master: return @@ -874,16 +858,14 @@ class BluesoundPlayer(MediaPlayerDevice): return self.send_bluesound_command(url) - @asyncio.coroutine - def async_clear_playlist(self): + async def async_clear_playlist(self): """Clear players playlist.""" if self.is_grouped and not self.is_master: return return self.send_bluesound_command('Clear') - @asyncio.coroutine - def async_media_next_track(self): + async def async_media_next_track(self): """Send media_next command to media player.""" if self.is_grouped and not self.is_master: return @@ -897,8 +879,7 @@ class BluesoundPlayer(MediaPlayerDevice): return self.send_bluesound_command(cmd) - @asyncio.coroutine - def async_media_previous_track(self): + async def async_media_previous_track(self): """Send media_previous command to media player.""" if self.is_grouped and not self.is_master: return @@ -912,40 +893,35 @@ class BluesoundPlayer(MediaPlayerDevice): return self.send_bluesound_command(cmd) - @asyncio.coroutine - def async_media_play(self): + async def async_media_play(self): """Send media_play command to media player.""" if self.is_grouped and not self.is_master: return return self.send_bluesound_command('Play') - @asyncio.coroutine - def async_media_pause(self): + async def async_media_pause(self): """Send media_pause command to media player.""" if self.is_grouped and not self.is_master: return return self.send_bluesound_command('Pause') - @asyncio.coroutine - def async_media_stop(self): + async def async_media_stop(self): """Send stop command.""" if self.is_grouped and not self.is_master: return return self.send_bluesound_command('Pause') - @asyncio.coroutine - def async_media_seek(self, position): + async def async_media_seek(self, position): """Send media_seek command to media player.""" if self.is_grouped and not self.is_master: return return self.send_bluesound_command('Play?seek=' + str(float(position))) - @asyncio.coroutine - def async_play_media(self, media_type, media_id, **kwargs): + async def async_play_media(self, media_type, media_id, **kwargs): """ Send the play_media command to the media player. @@ -961,24 +937,21 @@ class BluesoundPlayer(MediaPlayerDevice): return self.send_bluesound_command(url) - @asyncio.coroutine - def async_volume_up(self): + async def async_volume_up(self): """Volume up the media player.""" current_vol = self.volume_level if not current_vol or current_vol < 0: return return self.async_set_volume_level(((current_vol*100)+1)/100) - @asyncio.coroutine - def async_volume_down(self): + async def async_volume_down(self): """Volume down the media player.""" current_vol = self.volume_level if not current_vol or current_vol < 0: return return self.async_set_volume_level(((current_vol*100)-1)/100) - @asyncio.coroutine - def async_set_volume_level(self, volume): + async def async_set_volume_level(self, volume): """Send volume_up command to media player.""" if volume < 0: volume = 0 @@ -987,8 +960,7 @@ class BluesoundPlayer(MediaPlayerDevice): return self.send_bluesound_command( 'Volume?level=' + str(float(volume) * 100)) - @asyncio.coroutine - def async_mute_volume(self, mute): + async def async_mute_volume(self, mute): """Send mute command to media player.""" if mute: volume = self.volume_level From f5093b474a010a8059e148417f5a2484e1cff90b Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Sat, 17 Mar 2018 12:27:21 +0100 Subject: [PATCH 123/220] Python 3.5 async with (#13283) --- homeassistant/helpers/entity_platform.py | 2 +- homeassistant/helpers/restore_state.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 712121bbdb5..501ab5057a3 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -311,7 +311,7 @@ class EntityPlatform(object): self.scan_interval) return - with (await self._process_updates): + async with self._process_updates: tasks = [] for entity in self.entities.values(): if not entity.should_poll: diff --git a/homeassistant/helpers/restore_state.py b/homeassistant/helpers/restore_state.py index aac00b07d7a..eb88a3db369 100644 --- a/homeassistant/helpers/restore_state.py +++ b/homeassistant/helpers/restore_state.py @@ -75,7 +75,7 @@ async def async_get_last_state(hass, entity_id: str): if _LOCK not in hass.data: hass.data[_LOCK] = asyncio.Lock(loop=hass.loop) - with (await hass.data[_LOCK]): + async with hass.data[_LOCK]: if DATA_RESTORE_CACHE not in hass.data: await hass.async_add_job( _load_restore_cache, hass) From 3442b6741d969def6b97b99d8bc2c978252efed0 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Sat, 17 Mar 2018 13:14:53 +0100 Subject: [PATCH 124/220] Fix WUnderground duplicate entity ids (#13285) * Fix WUnderground duplicate entity ids * Entity Namespace --- .../components/sensor/wunderground.py | 25 ++++++++++++++++--- homeassistant/helpers/entity.py | 4 +-- homeassistant/util/__init__.py | 4 +-- tests/components/sensor/test_wunderground.py | 20 +++++++++++++++ 4 files changed, 45 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/sensor/wunderground.py b/homeassistant/components/sensor/wunderground.py index 0375bb1344c..7938b17e4d6 100644 --- a/homeassistant/components/sensor/wunderground.py +++ b/homeassistant/components/sensor/wunderground.py @@ -14,11 +14,12 @@ import async_timeout import voluptuous as vol from homeassistant.helpers.typing import HomeAssistantType, ConfigType +from homeassistant.components import sensor from homeassistant.components.sensor import PLATFORM_SCHEMA, ENTITY_ID_FORMAT from homeassistant.const import ( CONF_MONITORED_CONDITIONS, CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, TEMP_FAHRENHEIT, TEMP_CELSIUS, LENGTH_INCHES, LENGTH_KILOMETERS, - LENGTH_MILES, LENGTH_FEET, ATTR_ATTRIBUTION) + LENGTH_MILES, LENGTH_FEET, ATTR_ATTRIBUTION, CONF_ENTITY_NAMESPACE) from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.entity import Entity, async_generate_entity_id from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -617,6 +618,8 @@ LANG_CODES = [ 'CY', 'SN', 'JI', 'YI', ] +DEFAULT_ENTITY_NAMESPACE = 'pws' + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_API_KEY): cv.string, vol.Optional(CONF_PWS_ID): cv.string, @@ -627,22 +630,31 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ 'Latitude and longitude must exist together'): cv.longitude, vol.Required(CONF_MONITORED_CONDITIONS): vol.All(cv.ensure_list, vol.Length(min=1), [vol.In(SENSOR_TYPES)]), + vol.Optional(CONF_ENTITY_NAMESPACE, + default=DEFAULT_ENTITY_NAMESPACE): cv.string, }) +# Stores a list of entity ids we added in order to support multiple stations +# at once. +ADDED_ENTITY_IDS_KEY = 'wunderground_added_entity_ids' + @asyncio.coroutine def async_setup_platform(hass: HomeAssistantType, config: ConfigType, async_add_devices, discovery_info=None): """Set up the WUnderground sensor.""" + hass.data.setdefault(ADDED_ENTITY_IDS_KEY, set()) + latitude = config.get(CONF_LATITUDE, hass.config.latitude) longitude = config.get(CONF_LONGITUDE, hass.config.longitude) + namespace = config.get(CONF_ENTITY_NAMESPACE) rest = WUndergroundData( hass, config.get(CONF_API_KEY), config.get(CONF_PWS_ID), config.get(CONF_LANG), latitude, longitude) sensors = [] for variable in config[CONF_MONITORED_CONDITIONS]: - sensors.append(WUndergroundSensor(hass, rest, variable)) + sensors.append(WUndergroundSensor(hass, rest, variable, namespace)) yield from rest.async_update() if not rest.data: @@ -654,7 +666,8 @@ def async_setup_platform(hass: HomeAssistantType, config: ConfigType, class WUndergroundSensor(Entity): """Implementing the WUnderground sensor.""" - def __init__(self, hass: HomeAssistantType, rest, condition): + def __init__(self, hass: HomeAssistantType, rest, condition, + namespace: str): """Initialize the sensor.""" self.rest = rest self._condition = condition @@ -666,8 +679,12 @@ class WUndergroundSensor(Entity): self._entity_picture = None self._unit_of_measurement = self._cfg_expand("unit_of_measurement") self.rest.request_feature(SENSOR_TYPES[condition].feature) + current_ids = set(hass.states.async_entity_ids(sensor.DOMAIN)) + current_ids |= hass.data[ADDED_ENTITY_IDS_KEY] self.entity_id = async_generate_entity_id( - ENTITY_ID_FORMAT, "pws_" + condition, hass=hass) + ENTITY_ID_FORMAT, "{} {}".format(namespace, condition), + current_ids=current_ids) + hass.data[ADDED_ENTITY_IDS_KEY].add(self.entity_id) def _cfg_expand(self, what, default=None): """Parse and return sensor data.""" diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 4efe8d2f6c3..efaefc26184 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -4,7 +4,7 @@ import logging import functools as ft from timeit import default_timer as timer -from typing import Optional, List +from typing import Optional, List, Iterable from homeassistant.const import ( ATTR_ASSUMED_STATE, ATTR_FRIENDLY_NAME, ATTR_HIDDEN, ATTR_ICON, @@ -42,7 +42,7 @@ def generate_entity_id(entity_id_format: str, name: Optional[str], @callback def async_generate_entity_id(entity_id_format: str, name: Optional[str], - current_ids: Optional[List[str]] = None, + current_ids: Optional[Iterable[str]] = None, hass: Optional[HomeAssistant] = None) -> str: """Generate a unique entity ID based on given entity IDs or used IDs.""" if current_ids is None: diff --git a/homeassistant/util/__init__.py b/homeassistant/util/__init__.py index 82ba6a734f8..a8a84c6c880 100644 --- a/homeassistant/util/__init__.py +++ b/homeassistant/util/__init__.py @@ -13,7 +13,7 @@ from functools import wraps from types import MappingProxyType from unicodedata import normalize -from typing import Any, Optional, TypeVar, Callable, Sequence, KeysView, Union +from typing import Any, Optional, TypeVar, Callable, KeysView, Union, Iterable from .dt import as_local, utcnow @@ -72,7 +72,7 @@ def convert(value: T, to_type: Callable[[T], U], def ensure_unique_string(preferred_string: str, current_strings: - Union[Sequence[str], KeysView[str]]) -> str: + Union[Iterable[str], KeysView[str]]) -> str: """Return a string that is not present in current_strings. If preferred string exists will append _2, _3, .. diff --git a/tests/components/sensor/test_wunderground.py b/tests/components/sensor/test_wunderground.py index 27047ba0ad0..65526e2d938 100644 --- a/tests/components/sensor/test_wunderground.py +++ b/tests/components/sensor/test_wunderground.py @@ -143,3 +143,23 @@ def test_invalid_data(hass, aioclient_mock): for condition in VALID_CONFIG['monitored_conditions']: state = hass.states.get('sensor.pws_' + condition) assert state.state == STATE_UNKNOWN + + +async def test_entity_id_with_multiple_stations(hass, aioclient_mock): + """Test not generating duplicate entity ids with multiple stations.""" + aioclient_mock.get(URL, text=load_fixture('wunderground-valid.json')) + + config = [ + VALID_CONFIG, + {**VALID_CONFIG, 'entity_namespace': 'hi'} + ] + await async_setup_component(hass, 'sensor', {'sensor': config}) + await hass.async_block_till_done() + + state = hass.states.get('sensor.pws_weather') + assert state is not None + assert state.state == 'Clear' + + state = hass.states.get('sensor.hi_weather') + assert state is not None + assert state.state == 'Clear' From 8fed405da7ede24a57dee08e189bf90ccf05657e Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 17 Mar 2018 17:37:09 +0100 Subject: [PATCH 125/220] Upgrade aiohttp to 3.0.9 (#13288) --- 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 c91d7c84aa9..814a4679e1d 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -5,7 +5,7 @@ pip>=8.0.3 jinja2>=2.10 voluptuous==0.11.1 typing>=3,<4 -aiohttp==3.0.7 +aiohttp==3.0.9 async_timeout==2.0.0 astral==1.6 certifi>=2017.4.17 diff --git a/requirements_all.txt b/requirements_all.txt index 839987611bc..c53084d4da2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -6,7 +6,7 @@ pip>=8.0.3 jinja2>=2.10 voluptuous==0.11.1 typing>=3,<4 -aiohttp==3.0.7 +aiohttp==3.0.9 async_timeout==2.0.0 astral==1.6 certifi>=2017.4.17 diff --git a/setup.py b/setup.py index 2e44258c619..816458459f2 100755 --- a/setup.py +++ b/setup.py @@ -49,7 +49,7 @@ REQUIRES = [ 'jinja2>=2.10', 'voluptuous==0.11.1', 'typing>=3,<4', - 'aiohttp==3.0.7', + 'aiohttp==3.0.9', 'async_timeout==2.0.0', 'astral==1.6', 'certifi>=2017.4.17', From e01a0f91d6a8704469912c07569d70aab4194051 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 17 Mar 2018 17:37:53 +0100 Subject: [PATCH 126/220] Upgrade aiohttp_cors to 0.7.0 (#13289) --- homeassistant/components/http/__init__.py | 3 +-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index 4d313b5132e..17906157a6e 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -4,7 +4,6 @@ This module provides WSGI application to serve the Home Assistant API. For more details about this component, please refer to the documentation at https://home-assistant.io/components/http/ """ - from ipaddress import ip_network import logging import os @@ -32,7 +31,7 @@ from .static import ( from .const import KEY_AUTHENTICATED, KEY_REAL_IP # noqa from .view import HomeAssistantView # noqa -REQUIREMENTS = ['aiohttp_cors==0.6.0'] +REQUIREMENTS = ['aiohttp_cors==0.7.0'] DOMAIN = 'http' diff --git a/requirements_all.txt b/requirements_all.txt index c53084d4da2..ed3f9fea94e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -71,7 +71,7 @@ aiodns==1.1.1 # homeassistant.components.emulated_hue # homeassistant.components.http -aiohttp_cors==0.6.0 +aiohttp_cors==0.7.0 # homeassistant.components.hue aiohue==1.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d41f9589de2..6dee6b37c7d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -32,7 +32,7 @@ aioautomatic==0.6.5 # homeassistant.components.emulated_hue # homeassistant.components.http -aiohttp_cors==0.6.0 +aiohttp_cors==0.7.0 # homeassistant.components.hue aiohue==1.2.0 From aec61b7c86f182975a60750359546e68139cb434 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 17 Mar 2018 17:39:24 +0100 Subject: [PATCH 127/220] Upgrade sqlalchemy to 1.2.5 (#13292) --- homeassistant/components/recorder/__init__.py | 13 +++++-------- homeassistant/components/sensor/sql.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 8 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index 23c073ff80a..f10e0fc75d7 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -35,7 +35,7 @@ from . import migration, purge from .const import DATA_INSTANCE from .util import session_scope -REQUIREMENTS = ['sqlalchemy==1.2.2'] +REQUIREMENTS = ['sqlalchemy==1.2.5'] _LOGGER = logging.getLogger(__name__) @@ -64,16 +64,13 @@ CONNECT_RETRY_WAIT = 3 FILTER_SCHEMA = vol.Schema({ vol.Optional(CONF_EXCLUDE, default={}): vol.Schema({ + vol.Optional(CONF_DOMAINS): vol.All(cv.ensure_list, [cv.string]), vol.Optional(CONF_ENTITIES): cv.entity_ids, - vol.Optional(CONF_DOMAINS): - vol.All(cv.ensure_list, [cv.string]), - vol.Optional(CONF_EVENT_TYPES): - vol.All(cv.ensure_list, [cv.string]) + vol.Optional(CONF_EVENT_TYPES): vol.All(cv.ensure_list, [cv.string]), }), vol.Optional(CONF_INCLUDE, default={}): vol.Schema({ + vol.Optional(CONF_DOMAINS): vol.All(cv.ensure_list, [cv.string]), vol.Optional(CONF_ENTITIES): cv.entity_ids, - vol.Optional(CONF_DOMAINS): - vol.All(cv.ensure_list, [cv.string]) }) }) @@ -255,7 +252,7 @@ class Recorder(threading.Thread): self.hass.add_job(register) result = hass_started.result() - # If shutdown happened before HASS finished starting + # If shutdown happened before Home Assistant finished starting if result is shutdown_task: return diff --git a/homeassistant/components/sensor/sql.py b/homeassistant/components/sensor/sql.py index 5d5d61ff822..af9fa233d40 100644 --- a/homeassistant/components/sensor/sql.py +++ b/homeassistant/components/sensor/sql.py @@ -19,7 +19,7 @@ from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['sqlalchemy==1.2.2'] +REQUIREMENTS = ['sqlalchemy==1.2.5'] CONF_QUERIES = 'queries' CONF_QUERY = 'query' diff --git a/requirements_all.txt b/requirements_all.txt index ed3f9fea94e..66009b91c0e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1165,7 +1165,7 @@ spotcrime==1.0.3 # homeassistant.components.recorder # homeassistant.scripts.db_migrator # homeassistant.components.sensor.sql -sqlalchemy==1.2.2 +sqlalchemy==1.2.5 # homeassistant.components.statsd statsd==3.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6dee6b37c7d..4f2eafdac56 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -176,7 +176,7 @@ somecomfort==0.5.0 # homeassistant.components.recorder # homeassistant.scripts.db_migrator # homeassistant.components.sensor.sql -sqlalchemy==1.2.2 +sqlalchemy==1.2.5 # homeassistant.components.statsd statsd==3.2.1 From d35077271db8cbf23f4b2d57b46aad846eb2e968 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 17 Mar 2018 17:40:03 +0100 Subject: [PATCH 128/220] Upgrade TwitterAPI to 2.5.0 (#13287) --- homeassistant/components/notify/twitter.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/notify/twitter.py b/homeassistant/components/notify/twitter.py index db7de8e40a0..9489e05cfa5 100644 --- a/homeassistant/components/notify/twitter.py +++ b/homeassistant/components/notify/twitter.py @@ -19,7 +19,7 @@ from homeassistant.components.notify import ( from homeassistant.const import CONF_ACCESS_TOKEN, CONF_USERNAME from homeassistant.helpers.event import async_track_point_in_time -REQUIREMENTS = ['TwitterAPI==2.4.10'] +REQUIREMENTS = ['TwitterAPI==2.5.0'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 66009b91c0e..5fecb0ce5df 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -52,7 +52,7 @@ SoCo==0.14 TravisPy==0.3.5 # homeassistant.components.notify.twitter -TwitterAPI==2.4.10 +TwitterAPI==2.5.0 # homeassistant.components.notify.yessssms YesssSMS==0.1.1b3 From 82f59ba98488bd6abb6f0003095094106f25405a Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 17 Mar 2018 17:40:31 +0100 Subject: [PATCH 129/220] Upgrade numpy to 1.14.2 (#13291) --- homeassistant/components/binary_sensor/trend.py | 2 +- homeassistant/components/image_processing/opencv.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/binary_sensor/trend.py b/homeassistant/components/binary_sensor/trend.py index 031e0aa42e5..9b4598f3c42 100644 --- a/homeassistant/components/binary_sensor/trend.py +++ b/homeassistant/components/binary_sensor/trend.py @@ -23,7 +23,7 @@ from homeassistant.helpers.entity import generate_entity_id from homeassistant.helpers.event import async_track_state_change from homeassistant.util import utcnow -REQUIREMENTS = ['numpy==1.14.0'] +REQUIREMENTS = ['numpy==1.14.2'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/image_processing/opencv.py b/homeassistant/components/image_processing/opencv.py index df58e2e9dc4..18e74966a59 100644 --- a/homeassistant/components/image_processing/opencv.py +++ b/homeassistant/components/image_processing/opencv.py @@ -16,7 +16,7 @@ from homeassistant.components.image_processing import ( from homeassistant.core import split_entity_id import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['numpy==1.14.0'] +REQUIREMENTS = ['numpy==1.14.2'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 5fecb0ce5df..b4078136525 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -528,7 +528,7 @@ nuheat==0.3.0 # homeassistant.components.binary_sensor.trend # homeassistant.components.image_processing.opencv -numpy==1.14.0 +numpy==1.14.2 # homeassistant.components.google oauth2client==4.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4f2eafdac56..6e58464efe5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -99,7 +99,7 @@ mficlient==0.3.0 # homeassistant.components.binary_sensor.trend # homeassistant.components.image_processing.opencv -numpy==1.14.0 +numpy==1.14.2 # homeassistant.components.mqtt # homeassistant.components.shiftr From dbc59ad1a7029be2c243a3375edb326b5ddd5a87 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 17 Mar 2018 17:41:10 +0100 Subject: [PATCH 130/220] Upgrade python-telegram-bot to 10.0.1 (#13294) --- homeassistant/components/telegram_bot/__init__.py | 8 ++++---- requirements_all.txt | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/telegram_bot/__init__.py b/homeassistant/components/telegram_bot/__init__.py index 9e5d4cd9665..3041e7b41e0 100644 --- a/homeassistant/components/telegram_bot/__init__.py +++ b/homeassistant/components/telegram_bot/__init__.py @@ -22,7 +22,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.exceptions import TemplateError from homeassistant.setup import async_prepare_setup_platform -REQUIREMENTS = ['python-telegram-bot==9.0.0'] +REQUIREMENTS = ['python-telegram-bot==10.0.1'] _LOGGER = logging.getLogger(__name__) @@ -181,7 +181,7 @@ def load_data(hass, url=None, filepath=None, username=None, password=None, while retry_num < num_retries: req = requests.get(url, **params) if not req.ok: - _LOGGER.warning("Status code %s (retry #%s) loading %s.", + _LOGGER.warning("Status code %s (retry #%s) loading %s", req.status_code, retry_num + 1, url) else: data = io.BytesIO(req.content) @@ -189,10 +189,10 @@ def load_data(hass, url=None, filepath=None, username=None, password=None, data.seek(0) data.name = url return data - _LOGGER.warning("Empty data (retry #%s) in %s).", + _LOGGER.warning("Empty data (retry #%s) in %s)", retry_num + 1, url) retry_num += 1 - _LOGGER.warning("Can't load photo in %s after %s retries.", + _LOGGER.warning("Can't load photo in %s after %s retries", url, retry_num) elif filepath is not None: if hass.config.is_allowed_path(filepath): diff --git a/requirements_all.txt b/requirements_all.txt index b4078136525..33fb1789b46 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -979,7 +979,7 @@ python-synology==0.1.0 python-tado==0.2.2 # homeassistant.components.telegram_bot -python-telegram-bot==9.0.0 +python-telegram-bot==10.0.1 # homeassistant.components.sensor.twitch python-twitch==1.3.0 From 181eca4b455f870324d9420ad1db775c6f4f19c4 Mon Sep 17 00:00:00 2001 From: cgtobi Date: Sat, 17 Mar 2018 17:43:07 +0100 Subject: [PATCH 131/220] Upgrade python-forecastio to 1.4.0 (#13282) * Upgrade python-forecastio to 1.4.0 * Upgrade python-forecastio to 1.4.0 for sensor as well. --- homeassistant/components/sensor/darksky.py | 2 +- homeassistant/components/weather/darksky.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/sensor/darksky.py b/homeassistant/components/sensor/darksky.py index e224feb7db7..3049415c754 100644 --- a/homeassistant/components/sensor/darksky.py +++ b/homeassistant/components/sensor/darksky.py @@ -19,7 +19,7 @@ from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['python-forecastio==1.3.5'] +REQUIREMENTS = ['python-forecastio==1.4.0'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/weather/darksky.py b/homeassistant/components/weather/darksky.py index 139f8abfce6..52aa8c46046 100644 --- a/homeassistant/components/weather/darksky.py +++ b/homeassistant/components/weather/darksky.py @@ -19,7 +19,7 @@ from homeassistant.const import ( import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle -REQUIREMENTS = ['python-forecastio==1.3.5'] +REQUIREMENTS = ['python-forecastio==1.4.0'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 33fb1789b46..47b38a876f7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -918,7 +918,7 @@ python-etherscan-api==0.0.3 # homeassistant.components.sensor.darksky # homeassistant.components.weather.darksky -python-forecastio==1.3.5 +python-forecastio==1.4.0 # homeassistant.components.gc100 python-gc100==1.0.3a diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6e58464efe5..0a7c3b493d4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -144,7 +144,7 @@ pynx584==0.4 # homeassistant.components.sensor.darksky # homeassistant.components.weather.darksky -python-forecastio==1.3.5 +python-forecastio==1.4.0 # homeassistant.components.sensor.whois pythonwhois==2.4.3 From 4d3743f3f79a1127208ce885ae501991ec3e704e Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Sat, 17 Mar 2018 19:08:52 +0100 Subject: [PATCH 132/220] Delete .gitmodules (#13295) --- .gitmodules | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 .gitmodules diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index e69de29bb2d..00000000000 From d042b3d7d109a2a94b45a51d8e0a16e421e23515 Mon Sep 17 00:00:00 2001 From: cburgess Date: Sat, 17 Mar 2018 17:35:16 -0700 Subject: [PATCH 133/220] Update to latest python-nest (#12590) Due to an upstream bug some devices will be assigned a where_id that is not visible in the nest API. As a result we can't get a friendly name for the where_id. A workaround has been released for python-nest in version 3.7.0. Update the home assistant requirements to python-nest==3.7.0 to work around this issue. References: https://nestdevelopers.io/t/missing-where-name-from-some-devices/1202 https://github.com/jkoelker/python-nest/issues/127 https://github.com/jkoelker/python-nest/pull/128 Fixes #12589 Fixes #12950 Fixes #13074 --- homeassistant/components/nest.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/nest.py b/homeassistant/components/nest.py index 37028decf71..e7d2ba90438 100644 --- a/homeassistant/components/nest.py +++ b/homeassistant/components/nest.py @@ -15,7 +15,7 @@ from homeassistant.const import ( CONF_STRUCTURE, CONF_FILENAME, CONF_BINARY_SENSORS, CONF_SENSORS, CONF_MONITORED_CONDITIONS) -REQUIREMENTS = ['python-nest==3.1.0'] +REQUIREMENTS = ['python-nest==3.7.0'] _CONFIGURING = {} _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 47b38a876f7..797c251be76 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -952,7 +952,7 @@ python-mpd2==0.5.5 python-mystrom==0.3.8 # homeassistant.components.nest -python-nest==3.1.0 +python-nest==3.7.0 # homeassistant.components.device_tracker.nmap_tracker python-nmap==0.6.1 From 8ed302402603c60ff71d5c0baebec269b3769b46 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 18 Mar 2018 01:37:31 +0100 Subject: [PATCH 134/220] Upgrade async_timeout to 2.0.1 (#13290) --- 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 814a4679e1d..e43e1f3dafe 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -6,7 +6,7 @@ jinja2>=2.10 voluptuous==0.11.1 typing>=3,<4 aiohttp==3.0.9 -async_timeout==2.0.0 +async_timeout==2.0.1 astral==1.6 certifi>=2017.4.17 attrs==17.4.0 diff --git a/requirements_all.txt b/requirements_all.txt index 797c251be76..2da1c3a6990 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -7,7 +7,7 @@ jinja2>=2.10 voluptuous==0.11.1 typing>=3,<4 aiohttp==3.0.9 -async_timeout==2.0.0 +async_timeout==2.0.1 astral==1.6 certifi>=2017.4.17 attrs==17.4.0 diff --git a/setup.py b/setup.py index 816458459f2..a317aeb18f1 100755 --- a/setup.py +++ b/setup.py @@ -50,7 +50,7 @@ REQUIRES = [ 'voluptuous==0.11.1', 'typing>=3,<4', 'aiohttp==3.0.9', - 'async_timeout==2.0.0', + 'async_timeout==2.0.1', 'astral==1.6', 'certifi>=2017.4.17', 'attrs==17.4.0', From b45dad507a0275c91f5142c10ce420545324e965 Mon Sep 17 00:00:00 2001 From: Mattias Welponer Date: Sun, 18 Mar 2018 16:57:53 +0100 Subject: [PATCH 135/220] Add initial support fo HomematicIP components (#12761) * Add initial support fo HomematicIP components * Fix module import * Update reqirments file as well * Added HomematicIP files * Update to homematicip * Code cleanup based on highligted issues * Update of reqiremnets file as well * Fix dispatcher usage * Rename homematicip to homematicip_cloud --- .coveragerc | 3 + homeassistant/components/homematicip_cloud.py | 170 ++++++++++++ .../components/sensor/homematicip_cloud.py | 258 ++++++++++++++++++ requirements_all.txt | 3 + 4 files changed, 434 insertions(+) create mode 100644 homeassistant/components/homematicip_cloud.py create mode 100644 homeassistant/components/sensor/homematicip_cloud.py diff --git a/.coveragerc b/.coveragerc index 4da5343bf4f..d98048636c3 100644 --- a/.coveragerc +++ b/.coveragerc @@ -109,6 +109,9 @@ omit = homeassistant/components/homematic/__init__.py homeassistant/components/*/homematic.py + homeassistant/components/homematicip_cloud.py + homeassistant/components/*/homematicip_cloud.py + homeassistant/components/ihc/* homeassistant/components/*/ihc.py diff --git a/homeassistant/components/homematicip_cloud.py b/homeassistant/components/homematicip_cloud.py new file mode 100644 index 00000000000..a89678624eb --- /dev/null +++ b/homeassistant/components/homematicip_cloud.py @@ -0,0 +1,170 @@ +""" +Support for HomematicIP components. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/homematicip/ +""" + +import logging +from socket import timeout + +import voluptuous as vol +from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import (dispatcher_send, + async_dispatcher_connect) +from homeassistant.helpers.discovery import load_platform +from homeassistant.helpers.entity import Entity + +REQUIREMENTS = ['homematicip==0.8'] + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = 'homematicip_cloud' + +CONF_NAME = 'name' +CONF_ACCESSPOINT = 'accesspoint' +CONF_AUTHTOKEN = 'authtoken' + +CONFIG_SCHEMA = vol.Schema({ + vol.Optional(DOMAIN): [vol.Schema({ + vol.Optional(CONF_NAME, default=''): cv.string, + vol.Required(CONF_ACCESSPOINT): cv.string, + vol.Required(CONF_AUTHTOKEN): cv.string, + })], +}, extra=vol.ALLOW_EXTRA) + +EVENT_HOME_CHANGED = 'homematicip_home_changed' +EVENT_DEVICE_CHANGED = 'homematicip_device_changed' +EVENT_GROUP_CHANGED = 'homematicip_group_changed' +EVENT_SECURITY_CHANGED = 'homematicip_security_changed' +EVENT_JOURNAL_CHANGED = 'homematicip_journal_changed' + +ATTR_HOME_ID = 'home_id' +ATTR_HOME_LABEL = 'home_label' +ATTR_DEVICE_ID = 'device_id' +ATTR_DEVICE_LABEL = 'device_label' +ATTR_STATUS_UPDATE = 'status_update' +ATTR_FIRMWARE_STATE = 'firmware_state' +ATTR_LOW_BATTERY = 'low_battery' +ATTR_SABOTAGE = 'sabotage' +ATTR_RSSI = 'rssi' + + +def setup(hass, config): + """Set up the HomematicIP component.""" + # pylint: disable=import-error, no-name-in-module + from homematicip.home import Home + hass.data.setdefault(DOMAIN, {}) + homes = hass.data[DOMAIN] + accesspoints = config.get(DOMAIN, []) + + def _update_event(events): + """Handle incoming HomeMaticIP events.""" + for event in events: + etype = event['eventType'] + edata = event['data'] + if etype == 'DEVICE_CHANGED': + dispatcher_send(hass, EVENT_DEVICE_CHANGED, edata.id) + elif etype == 'GROUP_CHANGED': + dispatcher_send(hass, EVENT_GROUP_CHANGED, edata.id) + elif etype == 'HOME_CHANGED': + dispatcher_send(hass, EVENT_HOME_CHANGED, edata.id) + elif etype == 'JOURNAL_CHANGED': + dispatcher_send(hass, EVENT_SECURITY_CHANGED, edata.id) + return True + + for device in accesspoints: + name = device.get(CONF_NAME) + accesspoint = device.get(CONF_ACCESSPOINT) + authtoken = device.get(CONF_AUTHTOKEN) + + home = Home() + if name.lower() == 'none': + name = '' + home.label = name + try: + home.set_auth_token(authtoken) + home.init(accesspoint) + if home.get_current_state(): + _LOGGER.info("Connection to HMIP established") + else: + _LOGGER.warning("Connection to HMIP could not be established") + return False + except timeout: + _LOGGER.warning("Connection to HMIP could not be established") + return False + homes[home.id] = home + home.onEvent += _update_event + home.enable_events() + _LOGGER.info('HUB name: %s, id: %s', home.label, home.id) + + for component in ['sensor']: + load_platform(hass, component, DOMAIN, + {'homeid': home.id}, config) + return True + + +class HomematicipGenericDevice(Entity): + """Representation of an HomematicIP generic device.""" + + def __init__(self, hass, home, device, signal=None): + """Initialize the generic device.""" + self.hass = hass + self._home = home + self._device = device + async_dispatcher_connect( + self.hass, EVENT_DEVICE_CHANGED, self._device_changed) + + @callback + def _device_changed(self, deviceid): + """Handle device state changes.""" + if deviceid is None or deviceid == self._device.id: + _LOGGER.debug('Event device %s', self._device.label) + self.async_schedule_update_ha_state() + + def _name(self, addon=''): + """Return the name of the device.""" + name = '' + if self._home.label != '': + name += self._home.label + ' ' + name += self._device.label + if addon != '': + name += ' ' + addon + return name + + @property + def name(self): + """Return the name of the generic device.""" + return self._name() + + @property + def should_poll(self): + """No polling needed.""" + return False + + @property + def available(self): + """Device available.""" + return not self._device.unreach + + def _generic_state_attributes(self): + """Return the state attributes of the generic device.""" + laststatus = '' + if self._device.lastStatusUpdate is not None: + laststatus = self._device.lastStatusUpdate.isoformat() + return { + ATTR_HOME_LABEL: self._home.label, + ATTR_DEVICE_LABEL: self._device.label, + ATTR_HOME_ID: self._device.homeId, + ATTR_DEVICE_ID: self._device.id.lower(), + ATTR_STATUS_UPDATE: laststatus, + ATTR_FIRMWARE_STATE: self._device.updateState.lower(), + ATTR_LOW_BATTERY: self._device.lowBat, + ATTR_RSSI: self._device.rssiDeviceValue, + } + + @property + def device_state_attributes(self): + """Return the state attributes of the generic device.""" + return self._generic_state_attributes() diff --git a/homeassistant/components/sensor/homematicip_cloud.py b/homeassistant/components/sensor/homematicip_cloud.py new file mode 100644 index 00000000000..8f298bbb3f6 --- /dev/null +++ b/homeassistant/components/sensor/homematicip_cloud.py @@ -0,0 +1,258 @@ +""" +Support for HomematicIP sensors. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/homematicip/ +""" + +import logging + +from homeassistant.core import callback +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.dispatcher import dispatcher_connect +from homeassistant.components.homematicip_cloud import ( + HomematicipGenericDevice, DOMAIN, EVENT_HOME_CHANGED, + ATTR_HOME_LABEL, ATTR_HOME_ID, ATTR_LOW_BATTERY, ATTR_RSSI) +from homeassistant.const import TEMP_CELSIUS, STATE_OK + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['homematicip_cloud'] + +ATTR_VALVE_STATE = 'valve_state' +ATTR_VALVE_POSITION = 'valve_position' +ATTR_TEMPERATURE_OFFSET = 'temperature_offset' + +HMIP_UPTODATE = 'up_to_date' +HMIP_VALVE_DONE = 'adaption_done' +HMIP_SABOTAGE = 'sabotage' + +STATE_LOW_BATTERY = 'low_battery' +STATE_SABOTAGE = 'sabotage' + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the HomematicIP sensors devices.""" + # pylint: disable=import-error, no-name-in-module + from homematicip.device import ( + HeatingThermostat, TemperatureHumiditySensorWithoutDisplay, + TemperatureHumiditySensorDisplay) + + _LOGGER.info('Setting up HomeMaticIP accespoint & generic devices') + homeid = discovery_info['homeid'] + home = hass.data[DOMAIN][homeid] + devices = [HomematicipAccesspoint(hass, home)] + if home.devices is None: + return + for device in home.devices: + devices.append(HomematicipDeviceStatus(hass, home, device)) + if isinstance(device, HeatingThermostat): + devices.append(HomematicipHeatingThermostat(hass, home, device)) + if isinstance(device, TemperatureHumiditySensorWithoutDisplay): + devices.append(HomematicipSensorThermometer(hass, home, device)) + devices.append(HomematicipSensorHumidity(hass, home, device)) + if isinstance(device, TemperatureHumiditySensorDisplay): + devices.append(HomematicipSensorThermometer(hass, home, device)) + devices.append(HomematicipSensorHumidity(hass, home, device)) + add_devices(devices) + + +class HomematicipAccesspoint(Entity): + """Representation of an HomeMaticIP access point.""" + + def __init__(self, hass, home): + """Initialize the access point sensor.""" + self.hass = hass + self._home = home + dispatcher_connect( + self.hass, EVENT_HOME_CHANGED, self._home_changed) + _LOGGER.debug('Setting up access point %s', home.label) + + @callback + def _home_changed(self, deviceid): + """Handle device state changes.""" + if deviceid is None or deviceid == self._home.id: + _LOGGER.debug('Event access point %s', self._home.label) + self.async_schedule_update_ha_state() + + @property + def name(self): + """Return the name of the access point device.""" + if self._home.label == '': + return 'Access Point Status' + return '{} Access Point Status'.format(self._home.label) + + @property + def icon(self): + """Return the icon of the access point device.""" + return 'mdi:access-point-network' + + @property + def state(self): + """Return the state of the access point.""" + return self._home.dutyCycle + + @property + def available(self): + """Device available.""" + return self._home.connected + + @property + def device_state_attributes(self): + """Return the state attributes of the access point.""" + return { + ATTR_HOME_LABEL: self._home.label, + ATTR_HOME_ID: self._home.id, + } + + +class HomematicipDeviceStatus(HomematicipGenericDevice): + """Representation of an HomematicIP device status.""" + + def __init__(self, hass, home, device, signal=None): + """Initialize the device.""" + super().__init__(hass, home, device) + _LOGGER.debug('Setting up sensor device status: %s', device.label) + + @property + def name(self): + """Return the name of the device.""" + return self._name('Status') + + @property + def icon(self): + """Return the icon of the status device.""" + if (hasattr(self._device, 'sabotage') and + self._device.sabotage == HMIP_SABOTAGE): + return 'mdi:alert' + elif self._device.lowBat: + return 'mdi:battery-outline' + elif self._device.updateState.lower() != HMIP_UPTODATE: + return 'mdi:refresh' + return 'mdi:check' + + @property + def state(self): + """Return the state of the generic device.""" + if (hasattr(self._device, 'sabotage') and + self._device.sabotage == HMIP_SABOTAGE): + return STATE_SABOTAGE + elif self._device.lowBat: + return STATE_LOW_BATTERY + elif self._device.updateState.lower() != HMIP_UPTODATE: + return self._device.updateState.lower() + return STATE_OK + + +class HomematicipHeatingThermostat(HomematicipGenericDevice): + """MomematicIP heating thermostat representation.""" + + def __init__(self, hass, home, device): + """"Initialize heating thermostat.""" + super().__init__(hass, home, device) + _LOGGER.debug('Setting up heating thermostat device: %s', device.label) + + @property + def icon(self): + """Return the icon.""" + if self._device.valveState.lower() != HMIP_VALVE_DONE: + return 'mdi:alert' + return 'mdi:radiator' + + @property + def state(self): + """Return the state of the radiator valve.""" + if self._device.valveState.lower() != HMIP_VALVE_DONE: + return self._device.valveState.lower() + return round(self._device.valvePosition*100) + + @property + def unit_of_measurement(self): + """Return the unit this state is expressed in.""" + return '%' + + @property + def device_state_attributes(self): + """Return the state attributes.""" + return { + ATTR_VALVE_STATE: self._device.valveState.lower(), + ATTR_TEMPERATURE_OFFSET: self._device.temperatureOffset, + ATTR_LOW_BATTERY: self._device.lowBat, + ATTR_RSSI: self._device.rssiDeviceValue + } + + +class HomematicipSensorHumidity(HomematicipGenericDevice): + """MomematicIP thermometer device.""" + + def __init__(self, hass, home, device): + """"Initialize the thermometer device.""" + super().__init__(hass, home, device) + _LOGGER.debug('Setting up humidity device: %s', + device.label) + + @property + def name(self): + """Return the name of the device.""" + return self._name('Humidity') + + @property + def icon(self): + """Return the icon.""" + return 'mdi:water' + + @property + def state(self): + """Return the state.""" + return self._device.humidity + + @property + def unit_of_measurement(self): + """Return the unit this state is expressed in.""" + return '%' + + @property + def device_state_attributes(self): + """Return the state attributes.""" + return { + ATTR_LOW_BATTERY: self._device.lowBat, + ATTR_RSSI: self._device.rssiDeviceValue, + } + + +class HomematicipSensorThermometer(HomematicipGenericDevice): + """MomematicIP thermometer device.""" + + def __init__(self, hass, home, device): + """"Initialize the thermometer device.""" + super().__init__(hass, home, device) + _LOGGER.debug('Setting up thermometer device: %s', device.label) + + @property + def name(self): + """Return the name of the device.""" + return self._name('Temperature') + + @property + def icon(self): + """Return the icon.""" + return 'mdi:thermometer' + + @property + def state(self): + """Return the state.""" + return self._device.actualTemperature + + @property + def unit_of_measurement(self): + """Return the unit this state is expressed in.""" + return TEMP_CELSIUS + + @property + def device_state_attributes(self): + """Return the state attributes.""" + return { + ATTR_TEMPERATURE_OFFSET: self._device.temperatureOffset, + ATTR_LOW_BATTERY: self._device.lowBat, + ATTR_RSSI: self._device.rssiDeviceValue, + } diff --git a/requirements_all.txt b/requirements_all.txt index 2da1c3a6990..fbddbe9c448 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -358,6 +358,9 @@ holidays==0.9.4 # homeassistant.components.frontend home-assistant-frontend==20180316.0 +# homeassistant.components.homematicip_cloud +homematicip==0.8 + # homeassistant.components.camera.onvif http://github.com/tgaugry/suds-passworddigest-py3/archive/86fc50e39b4d2b8997481967d6a7fe1c57118999.zip#suds-passworddigest-py3==0.1.2a From 1e17b2fd63c9bcc3355015afb7127a59b1692c7b Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Sun, 18 Mar 2018 15:58:52 +0000 Subject: [PATCH 136/220] Added Time based SMA to Filter Sensor (#13104) * Added Time based SMA * move "now" to _filter_state() * Addressed comments * fix long line * type and name * # pylint: disable=redefined-builtin * added test --- homeassistant/components/sensor/filter.py | 66 ++++++++++++++++++++++- tests/components/sensor/test_filter.py | 18 ++++++- 2 files changed, 81 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sensor/filter.py b/homeassistant/components/sensor/filter.py index cde50699b29..aad7fec26a0 100644 --- a/homeassistant/components/sensor/filter.py +++ b/homeassistant/components/sensor/filter.py @@ -20,12 +20,14 @@ import homeassistant.helpers.config_validation as cv from homeassistant.util.decorator import Registry from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_state_change +import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) FILTER_NAME_LOWPASS = 'lowpass' FILTER_NAME_OUTLIER = 'outlier' FILTER_NAME_THROTTLE = 'throttle' +FILTER_NAME_TIME_SMA = 'time_simple_moving_average' FILTERS = Registry() CONF_FILTERS = 'filters' @@ -34,6 +36,9 @@ CONF_FILTER_WINDOW_SIZE = 'window_size' CONF_FILTER_PRECISION = 'precision' CONF_FILTER_RADIUS = 'radius' CONF_FILTER_TIME_CONSTANT = 'time_constant' +CONF_TIME_SMA_TYPE = 'type' + +TIME_SMA_LAST = 'last' DEFAULT_WINDOW_SIZE = 1 DEFAULT_PRECISION = 2 @@ -44,24 +49,37 @@ NAME_TEMPLATE = "{} filter" ICON = 'mdi:chart-line-variant' FILTER_SCHEMA = vol.Schema({ - vol.Optional(CONF_FILTER_WINDOW_SIZE, - default=DEFAULT_WINDOW_SIZE): vol.Coerce(int), vol.Optional(CONF_FILTER_PRECISION, default=DEFAULT_PRECISION): vol.Coerce(int), }) +# pylint: disable=redefined-builtin FILTER_OUTLIER_SCHEMA = FILTER_SCHEMA.extend({ vol.Required(CONF_FILTER_NAME): FILTER_NAME_OUTLIER, + vol.Optional(CONF_FILTER_WINDOW_SIZE, + default=DEFAULT_WINDOW_SIZE): vol.Coerce(int), vol.Optional(CONF_FILTER_RADIUS, default=DEFAULT_FILTER_RADIUS): vol.Coerce(float), }) FILTER_LOWPASS_SCHEMA = FILTER_SCHEMA.extend({ vol.Required(CONF_FILTER_NAME): FILTER_NAME_LOWPASS, + vol.Optional(CONF_FILTER_WINDOW_SIZE, + default=DEFAULT_WINDOW_SIZE): vol.Coerce(int), vol.Optional(CONF_FILTER_TIME_CONSTANT, default=DEFAULT_FILTER_TIME_CONSTANT): vol.Coerce(int), }) +FILTER_TIME_SMA_SCHEMA = FILTER_SCHEMA.extend({ + vol.Required(CONF_FILTER_NAME): FILTER_NAME_TIME_SMA, + vol.Optional(CONF_TIME_SMA_TYPE, + default=TIME_SMA_LAST): vol.In( + [None, TIME_SMA_LAST]), + + vol.Required(CONF_FILTER_WINDOW_SIZE): vol.All(cv.time_period, + cv.positive_timedelta) +}) + FILTER_THROTTLE_SCHEMA = FILTER_SCHEMA.extend({ vol.Required(CONF_FILTER_NAME): FILTER_NAME_THROTTLE, }) @@ -72,6 +90,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_FILTERS): vol.All(cv.ensure_list, [vol.Any(FILTER_OUTLIER_SCHEMA, FILTER_LOWPASS_SCHEMA, + FILTER_TIME_SMA_SCHEMA, FILTER_THROTTLE_SCHEMA)]) }) @@ -277,6 +296,49 @@ class LowPassFilter(Filter): return filtered +@FILTERS.register(FILTER_NAME_TIME_SMA) +class TimeSMAFilter(Filter): + """Simple Moving Average (SMA) Filter. + + The window_size is determined by time, and SMA is time weighted. + + Args: + variant (enum): type of argorithm used to connect discrete values + """ + + def __init__(self, window_size, precision, entity, type): + """Initialize Filter.""" + super().__init__(FILTER_NAME_TIME_SMA, 0, precision, entity) + self._time_window = int(window_size.total_seconds()) + self.last_leak = None + self.queue = deque() + + def _leak(self, now): + """Remove timeouted elements.""" + while self.queue: + timestamp, _ = self.queue[0] + if timestamp + self._time_window <= now: + self.last_leak = self.queue.popleft() + else: + return + + def _filter_state(self, new_state): + now = int(dt_util.utcnow().timestamp()) + + self._leak(now) + self.queue.append((now, float(new_state))) + moving_sum = 0 + start = now - self._time_window + _, prev_val = self.last_leak or (0, float(new_state)) + + for timestamp, val in self.queue: + moving_sum += (timestamp-start)*prev_val + start, prev_val = timestamp, val + moving_sum += (now-start)*prev_val + + return moving_sum/self._time_window + + @FILTERS.register(FILTER_NAME_THROTTLE) class ThrottleFilter(Filter): """Throttle Filter. diff --git a/tests/components/sensor/test_filter.py b/tests/components/sensor/test_filter.py index dd1112d65f8..0d4082731ab 100644 --- a/tests/components/sensor/test_filter.py +++ b/tests/components/sensor/test_filter.py @@ -1,8 +1,11 @@ """The test for the data filter sensor platform.""" +from datetime import timedelta import unittest +from unittest.mock import patch from homeassistant.components.sensor.filter import ( - LowPassFilter, OutlierFilter, ThrottleFilter) + LowPassFilter, OutlierFilter, ThrottleFilter, TimeSMAFilter) +import homeassistant.util.dt as dt_util from homeassistant.setup import setup_component from tests.common import get_test_home_assistant, assert_setup_component @@ -90,3 +93,16 @@ class TestFilterSensor(unittest.TestCase): if not filt.skip_processing: filtered.append(new_state) self.assertEqual([20, 21], filtered) + + def test_time_sma(self): + """Test if time_sma filter works.""" + filt = TimeSMAFilter(window_size=timedelta(minutes=2), + precision=2, + entity=None, + type='last') + past = dt_util.utcnow() - timedelta(minutes=5) + for state in self.values: + with patch('homeassistant.util.dt.utcnow', return_value=past): + filtered = filt.filter_state(state) + past += timedelta(minutes=1) + self.assertEqual(21.5, filtered) From 022d8fb816bde0676f0cb9cbda6cd8efe817064b Mon Sep 17 00:00:00 2001 From: maxclaey Date: Sun, 18 Mar 2018 17:00:08 +0100 Subject: [PATCH 137/220] Support for security systems controlled by IFTTT (#12975) * Add IFTTT alarm control panel * Update .coveragerc * Add support for code * Bugfix * Fix logging problem * Pin requirements * Update requirements_all.txt * Fix lint errors * Use ifttt component as a dependency instead of interacting with ifttt manually Take into account review comments * No default value for code * Take into account review comments * Provide a "push_alarm_state" service to change the state from IFTTT * Add service description * Fix @balloob review comments. Thanks! * Fix service description name --- .coveragerc | 1 + .../components/alarm_control_panel/ifttt.py | 146 ++++++++++++++++++ .../alarm_control_panel/services.yaml | 10 ++ 3 files changed, 157 insertions(+) create mode 100644 homeassistant/components/alarm_control_panel/ifttt.py diff --git a/.coveragerc b/.coveragerc index d98048636c3..5e1bbe67144 100644 --- a/.coveragerc +++ b/.coveragerc @@ -312,6 +312,7 @@ omit = homeassistant/components/alarm_control_panel/canary.py homeassistant/components/alarm_control_panel/concord232.py homeassistant/components/alarm_control_panel/ialarm.py + homeassistant/components/alarm_control_panel/ifttt.py homeassistant/components/alarm_control_panel/manual_mqtt.py homeassistant/components/alarm_control_panel/nx584.py homeassistant/components/alarm_control_panel/simplisafe.py diff --git a/homeassistant/components/alarm_control_panel/ifttt.py b/homeassistant/components/alarm_control_panel/ifttt.py new file mode 100644 index 00000000000..eb1a8f8ed7d --- /dev/null +++ b/homeassistant/components/alarm_control_panel/ifttt.py @@ -0,0 +1,146 @@ +""" +Interfaces with alarm control panels that have to be controlled through IFTTT. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/alarm_control_panel.ifttt/ +""" +import logging + +import voluptuous as vol + +import homeassistant.components.alarm_control_panel as alarm +from homeassistant.components.alarm_control_panel import ( + DOMAIN, PLATFORM_SCHEMA) +from homeassistant.components.ifttt import ( + ATTR_EVENT, DOMAIN as IFTTT_DOMAIN, SERVICE_TRIGGER) +from homeassistant.const import ( + ATTR_ENTITY_ID, ATTR_STATE, CONF_NAME, CONF_CODE, + STATE_ALARM_DISARMED, STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_AWAY) +import homeassistant.helpers.config_validation as cv + +DEPENDENCIES = ['ifttt'] + +_LOGGER = logging.getLogger(__name__) + +ALLOWED_STATES = [ + STATE_ALARM_DISARMED, STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME] + +DATA_IFTTT_ALARM = 'ifttt_alarm' +DEFAULT_NAME = "Home" + +EVENT_ALARM_ARM_AWAY = "alarm_arm_away" +EVENT_ALARM_ARM_HOME = "alarm_arm_home" +EVENT_ALARM_ARM_NIGHT = "alarm_arm_night" +EVENT_ALARM_DISARM = "alarm_disarm" + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_CODE): cv.string, +}) + +SERVICE_PUSH_ALARM_STATE = "ifttt_push_alarm_state" + +PUSH_ALARM_STATE_SERVICE_SCHEMA = vol.Schema({ + vol.Required(ATTR_ENTITY_ID): cv.entity_ids, + vol.Required(ATTR_STATE): cv.string, +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up a control panel managed through IFTTT.""" + if DATA_IFTTT_ALARM not in hass.data: + hass.data[DATA_IFTTT_ALARM] = [] + + name = config.get(CONF_NAME) + code = config.get(CONF_CODE) + + alarmpanel = IFTTTAlarmPanel(name, code) + hass.data[DATA_IFTTT_ALARM].append(alarmpanel) + add_devices([alarmpanel]) + + async def push_state_update(service): + """Set the service state as device state attribute.""" + entity_ids = service.data.get(ATTR_ENTITY_ID) + state = service.data.get(ATTR_STATE) + devices = hass.data[DATA_IFTTT_ALARM] + if entity_ids: + devices = [d for d in devices if d.entity_id in entity_ids] + + for device in devices: + device.push_alarm_state(state) + device.async_schedule_update_ha_state() + + hass.services.register(DOMAIN, SERVICE_PUSH_ALARM_STATE, push_state_update, + schema=PUSH_ALARM_STATE_SERVICE_SCHEMA) + + +class IFTTTAlarmPanel(alarm.AlarmControlPanel): + """Representation of an alarm control panel controlled throught IFTTT.""" + + def __init__(self, name, code): + """Initialize the alarm control panel.""" + self._name = name + self._code = code + self._state = None + + @property + def name(self): + """Return the name of the device.""" + return self._name + + @property + def state(self): + """Return the state of the device.""" + return self._state + + @property + def assumed_state(self): + """Notify that this platform return an assumed state.""" + return True + + @property + def code_format(self): + """Return one or more characters.""" + return None if self._code is None else '.+' + + def alarm_disarm(self, code=None): + """Send disarm command.""" + if not self._check_code(code): + return + self.set_alarm_state(EVENT_ALARM_DISARM) + + def alarm_arm_away(self, code=None): + """Send arm away command.""" + if not self._check_code(code): + return + self.set_alarm_state(EVENT_ALARM_ARM_AWAY) + + def alarm_arm_home(self, code=None): + """Send arm home command.""" + if not self._check_code(code): + return + self.set_alarm_state(EVENT_ALARM_ARM_HOME) + + def alarm_arm_night(self, code=None): + """Send arm night command.""" + if not self._check_code(code): + return + self.set_alarm_state(EVENT_ALARM_ARM_NIGHT) + + def set_alarm_state(self, event): + """Call the IFTTT trigger service to change the alarm state.""" + data = {ATTR_EVENT: event} + + self.hass.services.call(IFTTT_DOMAIN, SERVICE_TRIGGER, data) + _LOGGER.debug("Called IFTTT component to trigger event %s", event) + + def push_alarm_state(self, value): + """Push the alarm state to the given value.""" + if value in ALLOWED_STATES: + _LOGGER.debug("Pushed the alarm state to %s", value) + self._state = value + + def _check_code(self, code): + return self._code is None or self._code == code diff --git a/homeassistant/components/alarm_control_panel/services.yaml b/homeassistant/components/alarm_control_panel/services.yaml index 72784c8178c..391de2033c7 100644 --- a/homeassistant/components/alarm_control_panel/services.yaml +++ b/homeassistant/components/alarm_control_panel/services.yaml @@ -69,3 +69,13 @@ alarmdecoder_alarm_toggle_chime: code: description: A required code to toggle the alarm control panel chime with. example: 1234 + +ifttt_push_alarm_state: + description: Update the alarm state to the specified value. + fields: + entity_id: + description: Name of the alarm control panel which state has to be updated. + example: 'alarm_control_panel.downstairs' + state: + description: The state to which the alarm control panel has to be set. + example: 'armed_night' From 1dcc51cbdfc4e262841b0fe734ca69ea6b4f960d Mon Sep 17 00:00:00 2001 From: uchagani Date: Sun, 18 Mar 2018 12:02:07 -0400 Subject: [PATCH 138/220] Add ecobee fan mode (#12732) * add ability to set fan on * add tests and change "not on" status to "auto" * hound fix * more hounds * I don't understand new lines * fix linting errors * more linting fixes * change method signature * lint fixes * hopefully last lint fix * correct temp ranges according to ecobee API docs * update dependency to latest version * update tests with values from new temp logic * fix linting issue * more linting fixes * add SUPPORT_FAN_MODE to capabilities * add fan_list to attributes. restore current fan state to OFF if fan is not running. change target high/low temps from null to target temp when not in auto mode. change target temp from null to high/low temp when in auto mode change mode attribute to climate_mode for consistency with other lists. * remove unused import * simplify logic * lint fixes * revert change for target temps --- homeassistant/components/climate/ecobee.py | 80 +++++++++++++++------- homeassistant/components/ecobee.py | 2 +- requirements_all.txt | 2 +- tests/components/climate/test_ecobee.py | 48 ++++++++----- 4 files changed, 88 insertions(+), 44 deletions(-) diff --git a/homeassistant/components/climate/ecobee.py b/homeassistant/components/climate/ecobee.py index 6a4253ceca7..e64c2d5000e 100644 --- a/homeassistant/components/climate/ecobee.py +++ b/homeassistant/components/climate/ecobee.py @@ -14,10 +14,10 @@ from homeassistant.components.climate import ( ATTR_TARGET_TEMP_LOW, ATTR_TARGET_TEMP_HIGH, SUPPORT_TARGET_TEMPERATURE, SUPPORT_AWAY_MODE, SUPPORT_HOLD_MODE, SUPPORT_OPERATION_MODE, SUPPORT_TARGET_HUMIDITY_LOW, SUPPORT_TARGET_HUMIDITY_HIGH, - SUPPORT_AUX_HEAT, SUPPORT_TARGET_TEMPERATURE_HIGH, - SUPPORT_TARGET_TEMPERATURE_LOW) + SUPPORT_AUX_HEAT, SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_FAN_MODE, + SUPPORT_TARGET_TEMPERATURE_LOW, STATE_OFF) from homeassistant.const import ( - ATTR_ENTITY_ID, STATE_OFF, STATE_ON, ATTR_TEMPERATURE, TEMP_FAHRENHEIT) + ATTR_ENTITY_ID, STATE_ON, ATTR_TEMPERATURE, TEMP_FAHRENHEIT) import homeassistant.helpers.config_validation as cv _CONFIGURING = {} @@ -50,7 +50,7 @@ SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_AWAY_MODE | SUPPORT_HOLD_MODE | SUPPORT_OPERATION_MODE | SUPPORT_TARGET_HUMIDITY_LOW | SUPPORT_TARGET_HUMIDITY_HIGH | SUPPORT_AUX_HEAT | SUPPORT_TARGET_TEMPERATURE_HIGH | - SUPPORT_TARGET_TEMPERATURE_LOW) + SUPPORT_TARGET_TEMPERATURE_LOW | SUPPORT_FAN_MODE) def setup_platform(hass, config, add_devices, discovery_info=None): @@ -122,6 +122,7 @@ class Thermostat(ClimateDevice): self._climate_list = self.climate_list self._operation_list = ['auto', 'auxHeatOnly', 'cool', 'heat', 'off'] + self._fan_list = ['auto', 'on'] self.update_without_throttle = False def update(self): @@ -180,24 +181,29 @@ class Thermostat(ClimateDevice): return self.thermostat['runtime']['desiredCool'] / 10.0 return None - @property - def desired_fan_mode(self): - """Return the desired fan mode of operation.""" - return self.thermostat['runtime']['desiredFanMode'] - @property def fan(self): - """Return the current fan state.""" + """Return the current fan status.""" if 'fan' in self.thermostat['equipmentStatus']: return STATE_ON return STATE_OFF + @property + def current_fan_mode(self): + """Return the fan setting.""" + return self.thermostat['runtime']['desiredFanMode'] + @property def current_hold_mode(self): """Return current hold mode.""" mode = self._current_hold_mode return None if mode == AWAY_MODE else mode + @property + def fan_list(self): + """Return the available fan modes.""" + return self._fan_list + @property def _current_hold_mode(self): events = self.thermostat['events'] @@ -206,7 +212,7 @@ class Thermostat(ClimateDevice): if event['type'] == 'hold': if event['holdClimateRef'] == 'away': if int(event['endDate'][0:4]) - \ - int(event['startDate'][0:4]) <= 1: + int(event['startDate'][0:4]) <= 1: # A temporary hold from away climate is a hold return 'away' # A permanent hold from away climate @@ -228,7 +234,7 @@ class Thermostat(ClimateDevice): def current_operation(self): """Return current operation.""" if self.operation_mode == 'auxHeatOnly' or \ - self.operation_mode == 'heatPump': + self.operation_mode == 'heatPump': return STATE_HEAT return self.operation_mode @@ -271,10 +277,11 @@ class Thermostat(ClimateDevice): operation = STATE_HEAT else: operation = status + return { "actual_humidity": self.thermostat['runtime']['actualHumidity'], "fan": self.fan, - "mode": self.mode, + "climate_mode": self.mode, "operation": operation, "climate_list": self.climate_list, "fan_min_on_time": self.fan_min_on_time @@ -342,25 +349,46 @@ class Thermostat(ClimateDevice): cool_temp_setpoint, heat_temp_setpoint, self.hold_preference()) _LOGGER.debug("Setting ecobee hold_temp to: heat=%s, is=%s, " - "cool=%s, is=%s", heat_temp, isinstance( - heat_temp, (int, float)), cool_temp, + "cool=%s, is=%s", heat_temp, + isinstance(heat_temp, (int, float)), cool_temp, isinstance(cool_temp, (int, float))) self.update_without_throttle = True + def set_fan_mode(self, fan_mode): + """Set the fan mode. Valid values are "on" or "auto".""" + if (fan_mode.lower() != STATE_ON) and (fan_mode.lower() != STATE_AUTO): + error = "Invalid fan_mode value: Valid values are 'on' or 'auto'" + _LOGGER.error(error) + return + + cool_temp = self.thermostat['runtime']['desiredCool'] / 10.0 + heat_temp = self.thermostat['runtime']['desiredHeat'] / 10.0 + self.data.ecobee.set_fan_mode(self.thermostat_index, fan_mode, + cool_temp, heat_temp, + self.hold_preference()) + + _LOGGER.info("Setting fan mode to: %s", fan_mode) + def set_temp_hold(self, temp): - """Set temperature hold in modes other than auto.""" - # Set arbitrary range when not in auto mode - if self.current_operation == STATE_HEAT: + """Set temperature hold in modes other than auto. + + Ecobee API: It is good practice to set the heat and cool hold + temperatures to be the same, if the thermostat is in either heat, cool, + auxHeatOnly, or off mode. If the thermostat is in auto mode, an + additional rule is required. The cool hold temperature must be greater + than the heat hold temperature by at least the amount in the + heatCoolMinDelta property. + https://www.ecobee.com/home/developer/api/examples/ex5.shtml + """ + if self.current_operation == STATE_HEAT or self.current_operation == \ + STATE_COOL: heat_temp = temp - cool_temp = temp + 20 - elif self.current_operation == STATE_COOL: - heat_temp = temp - 20 cool_temp = temp else: - # In auto mode set temperature between - heat_temp = temp - 10 - cool_temp = temp + 10 + delta = self.thermostat['settings']['heatCoolMinDelta'] / 10 + heat_temp = temp - delta + cool_temp = temp + delta self.set_auto_temp_hold(heat_temp, cool_temp) def set_temperature(self, **kwargs): @@ -369,8 +397,8 @@ class Thermostat(ClimateDevice): high_temp = kwargs.get(ATTR_TARGET_TEMP_HIGH) temp = kwargs.get(ATTR_TEMPERATURE) - if self.current_operation == STATE_AUTO and (low_temp is not None or - high_temp is not None): + if self.current_operation == STATE_AUTO and \ + (low_temp is not None or high_temp is not None): self.set_auto_temp_hold(low_temp, high_temp) elif temp is not None: self.set_temp_hold(temp) diff --git a/homeassistant/components/ecobee.py b/homeassistant/components/ecobee.py index 132e230c137..d1503dc74dc 100644 --- a/homeassistant/components/ecobee.py +++ b/homeassistant/components/ecobee.py @@ -16,7 +16,7 @@ from homeassistant.const import CONF_API_KEY from homeassistant.util import Throttle from homeassistant.util.json import save_json -REQUIREMENTS = ['python-ecobee-api==0.0.15'] +REQUIREMENTS = ['python-ecobee-api==0.0.17'] _CONFIGURING = {} _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index fbddbe9c448..37c7150c146 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -911,7 +911,7 @@ python-clementine-remote==1.0.1 python-digitalocean==1.13.2 # homeassistant.components.ecobee -python-ecobee-api==0.0.15 +python-ecobee-api==0.0.17 # homeassistant.components.climate.eq3btsmart # python-eq3bt==0.1.9 diff --git a/tests/components/climate/test_ecobee.py b/tests/components/climate/test_ecobee.py index 4732376fceb..eb843d8eb34 100644 --- a/tests/components/climate/test_ecobee.py +++ b/tests/components/climate/test_ecobee.py @@ -3,6 +3,7 @@ import unittest from unittest import mock import homeassistant.const as const import homeassistant.components.climate.ecobee as ecobee +from homeassistant.components.climate import STATE_OFF class TestEcobee(unittest.TestCase): @@ -23,6 +24,7 @@ class TestEcobee(unittest.TestCase): 'desiredFanMode': 'on'}, 'settings': {'hvacMode': 'auto', 'fanMinOnTime': 10, + 'heatCoolMinDelta': 50, 'holdAction': 'nextTransition'}, 'equipmentStatus': 'fan', 'events': [{'name': 'Event1', @@ -81,17 +83,17 @@ class TestEcobee(unittest.TestCase): def test_desired_fan_mode(self): """Test desired fan mode property.""" - self.assertEqual('on', self.thermostat.desired_fan_mode) + self.assertEqual('on', self.thermostat.current_fan_mode) self.ecobee['runtime']['desiredFanMode'] = 'auto' - self.assertEqual('auto', self.thermostat.desired_fan_mode) + self.assertEqual('auto', self.thermostat.current_fan_mode) def test_fan(self): """Test fan property.""" self.assertEqual(const.STATE_ON, self.thermostat.fan) self.ecobee['equipmentStatus'] = '' - self.assertEqual(const.STATE_OFF, self.thermostat.fan) + self.assertEqual(STATE_OFF, self.thermostat.fan) self.ecobee['equipmentStatus'] = 'heatPump, heatPump2' - self.assertEqual(const.STATE_OFF, self.thermostat.fan) + self.assertEqual(STATE_OFF, self.thermostat.fan) def test_current_hold_mode_away_temporary(self): """Test current hold mode when away.""" @@ -180,7 +182,7 @@ class TestEcobee(unittest.TestCase): 'climate_list': ['Climate1', 'Climate2'], 'fan': 'off', 'fan_min_on_time': 10, - 'mode': 'Climate1', + 'climate_mode': 'Climate1', 'operation': 'heat'}, self.thermostat.device_state_attributes) @@ -189,7 +191,7 @@ class TestEcobee(unittest.TestCase): 'climate_list': ['Climate1', 'Climate2'], 'fan': 'off', 'fan_min_on_time': 10, - 'mode': 'Climate1', + 'climate_mode': 'Climate1', 'operation': 'heat'}, self.thermostat.device_state_attributes) self.ecobee['equipmentStatus'] = 'compCool1' @@ -197,7 +199,7 @@ class TestEcobee(unittest.TestCase): 'climate_list': ['Climate1', 'Climate2'], 'fan': 'off', 'fan_min_on_time': 10, - 'mode': 'Climate1', + 'climate_mode': 'Climate1', 'operation': 'cool'}, self.thermostat.device_state_attributes) self.ecobee['equipmentStatus'] = '' @@ -205,7 +207,7 @@ class TestEcobee(unittest.TestCase): 'climate_list': ['Climate1', 'Climate2'], 'fan': 'off', 'fan_min_on_time': 10, - 'mode': 'Climate1', + 'climate_mode': 'Climate1', 'operation': 'idle'}, self.thermostat.device_state_attributes) @@ -214,7 +216,7 @@ class TestEcobee(unittest.TestCase): 'climate_list': ['Climate1', 'Climate2'], 'fan': 'off', 'fan_min_on_time': 10, - 'mode': 'Climate1', + 'climate_mode': 'Climate1', 'operation': 'Unknown'}, self.thermostat.device_state_attributes) @@ -321,7 +323,7 @@ class TestEcobee(unittest.TestCase): self.assertFalse(self.data.ecobee.delete_vacation.called) self.assertFalse(self.data.ecobee.resume_program.called) self.data.ecobee.set_hold_temp.assert_has_calls( - [mock.call(1, 40.0, 20.0, 'nextTransition')]) + [mock.call(1, 35.0, 25.0, 'nextTransition')]) self.assertFalse(self.data.ecobee.set_climate_hold.called) def test_set_auto_temp_hold(self): @@ -337,21 +339,21 @@ class TestEcobee(unittest.TestCase): self.data.reset_mock() self.thermostat.set_temp_hold(30.0) self.data.ecobee.set_hold_temp.assert_has_calls( - [mock.call(1, 40.0, 20.0, 'nextTransition')]) + [mock.call(1, 35.0, 25.0, 'nextTransition')]) # Heat mode self.data.reset_mock() self.ecobee['settings']['hvacMode'] = 'heat' self.thermostat.set_temp_hold(30) self.data.ecobee.set_hold_temp.assert_has_calls( - [mock.call(1, 50, 30, 'nextTransition')]) + [mock.call(1, 30, 30, 'nextTransition')]) # Cool mode self.data.reset_mock() self.ecobee['settings']['hvacMode'] = 'cool' self.thermostat.set_temp_hold(30) self.data.ecobee.set_hold_temp.assert_has_calls( - [mock.call(1, 30, 10, 'nextTransition')]) + [mock.call(1, 30, 30, 'nextTransition')]) def test_set_temperature(self): """Test set temperature.""" @@ -366,21 +368,21 @@ class TestEcobee(unittest.TestCase): self.data.reset_mock() self.thermostat.set_temperature(temperature=20) self.data.ecobee.set_hold_temp.assert_has_calls( - [mock.call(1, 30, 10, 'nextTransition')]) + [mock.call(1, 25, 15, 'nextTransition')]) # Cool -> Hold self.data.reset_mock() self.ecobee['settings']['hvacMode'] = 'cool' self.thermostat.set_temperature(temperature=20.5) self.data.ecobee.set_hold_temp.assert_has_calls( - [mock.call(1, 20.5, 0.5, 'nextTransition')]) + [mock.call(1, 20.5, 20.5, 'nextTransition')]) # Heat -> Hold self.data.reset_mock() self.ecobee['settings']['hvacMode'] = 'heat' self.thermostat.set_temperature(temperature=20) self.data.ecobee.set_hold_temp.assert_has_calls( - [mock.call(1, 40, 20, 'nextTransition')]) + [mock.call(1, 20, 20, 'nextTransition')]) # Heat -> Auto self.data.reset_mock() @@ -450,3 +452,17 @@ class TestEcobee(unittest.TestCase): """Test climate list property.""" self.assertEqual(['Climate1', 'Climate2'], self.thermostat.climate_list) + + def test_set_fan_mode_on(self): + """Test set fan mode to on.""" + self.data.reset_mock() + self.thermostat.set_fan_mode('on') + self.data.ecobee.set_fan_mode.assert_has_calls( + [mock.call(1, 'on', 20, 40, 'nextTransition')]) + + def test_set_fan_mode_auto(self): + """Test set fan mode to auto.""" + self.data.reset_mock() + self.thermostat.set_fan_mode('auto') + self.data.ecobee.set_fan_mode.assert_has_calls( + [mock.call(1, 'auto', 20, 40, 'nextTransition')]) From 9cb3c9034f5deeb333e6e68394a331f6b9896c4e Mon Sep 17 00:00:00 2001 From: Igor Bernstein Date: Sun, 18 Mar 2018 12:17:56 -0400 Subject: [PATCH 139/220] Zigbee fan (#12289) * wip: initial control * fix initial state * cosmetic cleanup * doc typo * lint * fixes * fix unknown bug * Lint --- homeassistant/components/fan/zha.py | 114 ++++++++++++++++++++++++++ homeassistant/components/zha/const.py | 1 + 2 files changed, 115 insertions(+) create mode 100644 homeassistant/components/fan/zha.py diff --git a/homeassistant/components/fan/zha.py b/homeassistant/components/fan/zha.py new file mode 100644 index 00000000000..3288a788e1f --- /dev/null +++ b/homeassistant/components/fan/zha.py @@ -0,0 +1,114 @@ +""" +Fans on Zigbee Home Automation networks. + +For more details on this platform, please refer to the documentation +at https://home-assistant.io/components/fan.zha/ +""" +import asyncio +import logging +from homeassistant.components import zha +from homeassistant.components.fan import ( + DOMAIN, FanEntity, SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH, + SUPPORT_SET_SPEED) +from homeassistant.const import STATE_UNKNOWN + +DEPENDENCIES = ['zha'] + +_LOGGER = logging.getLogger(__name__) + +# Additional speeds in zigbee's ZCL +# Spec is unclear as to what this value means. On King Of Fans HBUniversal +# receiver, this means Very High. +SPEED_ON = 'on' +# The fan speed is self-regulated +SPEED_AUTO = 'auto' +# When the heated/cooled space is occupied, the fan is always on +SPEED_SMART = 'smart' + +SPEED_LIST = [ + SPEED_OFF, + SPEED_LOW, + SPEED_MEDIUM, + SPEED_HIGH, + SPEED_ON, + SPEED_AUTO, + SPEED_SMART +] + +VALUE_TO_SPEED = {i: speed for i, speed in enumerate(SPEED_LIST)} +SPEED_TO_VALUE = {speed: i for i, speed in enumerate(SPEED_LIST)} + + +@asyncio.coroutine +def async_setup_platform(hass, config, async_add_devices, discovery_info=None): + """Set up the Zigbee Home Automation fans.""" + discovery_info = zha.get_discovery_info(hass, discovery_info) + if discovery_info is None: + return + + async_add_devices([ZhaFan(**discovery_info)], update_before_add=True) + + +class ZhaFan(zha.Entity, FanEntity): + """Representation of a ZHA fan.""" + + _domain = DOMAIN + + @property + def supported_features(self) -> int: + """Flag supported features.""" + return SUPPORT_SET_SPEED + + @property + def speed_list(self) -> list: + """Get the list of available speeds.""" + return SPEED_LIST + + @property + def speed(self) -> str: + """Return the current speed.""" + return self._state + + @property + def is_on(self) -> bool: + """Return true if entity is on.""" + if self._state == STATE_UNKNOWN: + return False + return self._state != SPEED_OFF + + @asyncio.coroutine + def async_turn_on(self, speed: str = None, **kwargs) -> None: + """Turn the entity on.""" + if speed is None: + speed = SPEED_MEDIUM + + yield from self.async_set_speed(speed) + + @asyncio.coroutine + def async_turn_off(self, **kwargs) -> None: + """Turn the entity off.""" + yield from self.async_set_speed(SPEED_OFF) + + @asyncio.coroutine + def async_set_speed(self: FanEntity, speed: str) -> None: + """Set the speed of the fan.""" + yield from self._endpoint.fan.write_attributes({ + 'fan_mode': SPEED_TO_VALUE[speed]}) + + self._state = speed + self.async_schedule_update_ha_state() + + @asyncio.coroutine + def async_update(self): + """Retrieve latest state.""" + result = yield from zha.safe_read(self._endpoint.fan, ['fan_mode']) + new_value = result.get('fan_mode', None) + self._state = VALUE_TO_SPEED.get(new_value, STATE_UNKNOWN) + + @property + def should_poll(self) -> bool: + """Return True if entity has to be polled for state. + + False if entity pushes its state to HA. + """ + return False diff --git a/homeassistant/components/zha/const.py b/homeassistant/components/zha/const.py index deaa1257396..4fe3581d5b2 100644 --- a/homeassistant/components/zha/const.py +++ b/homeassistant/components/zha/const.py @@ -36,6 +36,7 @@ def populate_data(): zcl.clusters.measurement.RelativeHumidity: 'sensor', zcl.clusters.measurement.TemperatureMeasurement: 'sensor', zcl.clusters.security.IasZone: 'binary_sensor', + zcl.clusters.hvac.Fan: 'fan', }) # A map of hass components to all Zigbee clusters it could use From 437ffc8337d778a1815424f57e6e8dc69e9bab76 Mon Sep 17 00:00:00 2001 From: Kevin Raddatz Date: Sun, 18 Mar 2018 17:25:25 +0100 Subject: [PATCH 140/220] Update plex.py (#12157) * Update plex.py show information about media depending if it is a movie or an episode set time_between_scans to 10 s to match with plex media_player component * Update plex.py lint * Update plex.py linting * Update plex.py linting * Update plex.py linting * Update plex.py added catch for tracks and everything else if no release year is given, instead of () it now show nothing * Update plex.py Remove the album year to match with the Plex UI * Update README.rst * Update README.rst * Update plex.py reformat code to make it more readable recorded tv shows might not have episode numbers assigned -> check before adding to title * Update plex.py cleanup excessive whitespace * Update plex.py cleanup excessive whitespace --- homeassistant/components/sensor/plex.py | 40 ++++++++++++++++++++++--- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/sensor/plex.py b/homeassistant/components/sensor/plex.py index 87af51d2bbd..505983cb3a7 100644 --- a/homeassistant/components/sensor/plex.py +++ b/homeassistant/components/sensor/plex.py @@ -115,9 +115,41 @@ class PlexSensor(Entity): sessions = self._server.sessions() now_playing = [] for sess in sessions: - user = sess.usernames[0] if sess.usernames is not None else "" - title = sess.title if sess.title is not None else "" - year = sess.year if sess.year is not None else "" - now_playing.append((user, "{0} ({1})".format(title, year))) + user = sess.usernames[0] + device = sess.players[0].title + now_playing_user = "{0} - {1}".format(user, device) + now_playing_title = "" + + if sess.TYPE == 'episode': + # example: + # "Supernatural (2005) - S01 · E13 - Route 666" + season_title = sess.grandparentTitle + if sess.show().year is not None: + season_title += " ({0})".format(sess.show().year) + season_episode = "S{0}".format(sess.parentIndex) + if sess.index is not None: + season_episode += " · E{1}".format(sess.index) + episode_title = sess.title + now_playing_title = "{0} - {1} - {2}".format(season_title, + season_episode, + episode_title) + elif sess.TYPE == 'track': + # example: + # "Billy Talent - Afraid of Heights - Afraid of Heights" + track_artist = sess.grandparentTitle + track_album = sess.parentTitle + track_title = sess.title + now_playing_title = "{0} - {1} - {2}".format(track_artist, + track_album, + track_title) + else: + # example: + # "picture_of_last_summer_camp (2015)" + # "The Incredible Hulk (2008)" + now_playing_title = sess.title + if sess.year is not None: + now_playing_title += " ({0})".format(sess.year) + + now_playing.append((now_playing_user, now_playing_title)) self._state = len(sessions) self._now_playing = now_playing From 1cbf9792d768255fb7a5e136de9c5b23bf2048d2 Mon Sep 17 00:00:00 2001 From: Erik Eriksson Date: Sun, 18 Mar 2018 17:26:07 +0100 Subject: [PATCH 141/220] Support MQTT Lock discovery (#13303) --- homeassistant/components/lock/mqtt.py | 3 +++ homeassistant/components/mqtt/discovery.py | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/lock/mqtt.py b/homeassistant/components/lock/mqtt.py index e73e35a9900..d8af22cd5c3 100644 --- a/homeassistant/components/lock/mqtt.py +++ b/homeassistant/components/lock/mqtt.py @@ -44,6 +44,9 @@ PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({ @asyncio.coroutine def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Set up the MQTT lock.""" + if discovery_info is not None: + config = PLATFORM_SCHEMA(discovery_info) + value_template = config.get(CONF_VALUE_TEMPLATE) if value_template is not None: value_template.hass = hass diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py index d0164706626..3263521f3f1 100644 --- a/homeassistant/components/mqtt/discovery.py +++ b/homeassistant/components/mqtt/discovery.py @@ -20,13 +20,14 @@ TOPIC_MATCHER = re.compile( r'(?:(?P[a-zA-Z0-9_-]+)/)?(?P[a-zA-Z0-9_-]+)/config') SUPPORTED_COMPONENTS = [ - 'binary_sensor', 'cover', 'fan', 'light', 'sensor', 'switch'] + 'binary_sensor', 'cover', 'fan', 'light', 'sensor', 'switch', 'lock'] ALLOWED_PLATFORMS = { 'binary_sensor': ['mqtt'], 'cover': ['mqtt'], 'fan': ['mqtt'], 'light': ['mqtt', 'mqtt_json', 'mqtt_template'], + 'lock': ['mqtt'], 'sensor': ['mqtt'], 'switch': ['mqtt'], } From 6b059489a62ac4c2008e7f79b1e65f1746c1f69d Mon Sep 17 00:00:00 2001 From: nielstron Date: Sun, 18 Mar 2018 17:26:33 +0100 Subject: [PATCH 142/220] Adding a discoverable Samsung Syncthru Printer sensor platform (#13134) * Added a simple component to support the BLNET Adds a component based on pyblnet, that hooks up the blnet to home assistant * Adds support for custimzation of blnet sensor devices * Setting up blnet as a platfrom * Updated use of state_attributes Now the friendly_name (and for digital values the mode) is set in the state_attributes whereas the name is defined as "blnet_(analog|digital)_{sensornumber}" so you can reliably add them to groups. * Added support for the SyncThru printer web service * Added pysyncthru to the requirements * Changed to Dependencis, import inside setup_platform * Switch back to REQUIREMENTS Looks like DEPENDENCIES is not meant for python packages but for other HA components * Fixed access to _attributes * Final fix * Several Bugfixes When the printer goes offline, the last state will be kept. Also now checks if the printer is reachable upon setup * Register syncthru as discoverable * Included possible conditions to monitor * Split the printer sensor in several seperate sensor entities * Fixed bug at sensor creation, pep8 conform * Bugfix * Bugfix * Removed Blnet components * Fixed unused import * Renamed discoverable to samsung_printer * Removed unused Attribute _friendly_name * Inserted missing space * Pinned requirements and added to coveragerc * Reduced redundancy by condensing into multiple sub-classes * Fixed indentation * Fixed super constructor calls * Fixed super constructor calls * Fixed format * Resolving style issues and using name instead of friendly_name * Pinned pysyncthru in requirements_all, having trouble with friendly_name * Iterating over dictionary instead of dict.keys() * ran gen_reqirements_all.py * Fixed flake 8 issues * Added a simple component to support the BLNET Adds a component based on pyblnet, that hooks up the blnet to home assistant * Implemented requested changes * raised dependecies to pysyncthru version that has timeouts * Raised required version for full timeout support * Adds support for custimzation of blnet sensor devices * Setting up blnet as a platfrom * Updated use of state_attributes Now the friendly_name (and for digital values the mode) is set in the state_attributes whereas the name is defined as "blnet_(analog|digital)_{sensornumber}" so you can reliably add them to groups. * Added support for the SyncThru printer web service * Added pysyncthru to the requirements * Removed Blnet components * Pinned requirements and added to coveragerc * Fixed indentation * Fixed format * Pinned pysyncthru in requirements_all, having trouble with friendly_name * ran gen_reqirements_all.py * Updated requirements_all * Renamed sensor objects, removed passing of hass entity * Removed merge artifacts * Reset syncthru to newest state * Updated requirements_all * switched to using the newest version of pysyncthru * Sorted coveragerc --- .coveragerc | 1 + homeassistant/components/discovery.py | 2 + homeassistant/components/sensor/syncthru.py | 233 ++++++++++++++++++++ requirements_all.txt | 3 + 4 files changed, 239 insertions(+) create mode 100644 homeassistant/components/sensor/syncthru.py diff --git a/.coveragerc b/.coveragerc index 5e1bbe67144..40fccd5e921 100644 --- a/.coveragerc +++ b/.coveragerc @@ -655,6 +655,7 @@ omit = homeassistant/components/sensor/supervisord.py homeassistant/components/sensor/swiss_hydrological_data.py homeassistant/components/sensor/swiss_public_transport.py + homeassistant/components/sensor/syncthru.py homeassistant/components/sensor/synologydsm.py homeassistant/components/sensor/systemmonitor.py homeassistant/components/sensor/sytadin.py diff --git a/homeassistant/components/discovery.py b/homeassistant/components/discovery.py index 6ab7f42558b..eb53782d698 100644 --- a/homeassistant/components/discovery.py +++ b/homeassistant/components/discovery.py @@ -38,6 +38,7 @@ SERVICE_TELLDUSLIVE = 'tellstick' SERVICE_HUE = 'philips_hue' SERVICE_DECONZ = 'deconz' SERVICE_DAIKIN = 'daikin' +SERVICE_SAMSUNG_PRINTER = 'samsung_printer' SERVICE_HANDLERS = { SERVICE_HASS_IOS_APP: ('ios', None), @@ -53,6 +54,7 @@ SERVICE_HANDLERS = { SERVICE_HUE: ('hue', None), SERVICE_DECONZ: ('deconz', None), SERVICE_DAIKIN: ('daikin', None), + SERVICE_SAMSUNG_PRINTER: ('sensor', 'syncthru'), 'google_cast': ('media_player', 'cast'), 'panasonic_viera': ('media_player', 'panasonic_viera'), 'plex_mediaserver': ('media_player', 'plex'), diff --git a/homeassistant/components/sensor/syncthru.py b/homeassistant/components/sensor/syncthru.py new file mode 100644 index 00000000000..a24482bda01 --- /dev/null +++ b/homeassistant/components/sensor/syncthru.py @@ -0,0 +1,233 @@ +""" +Support for Samsung Printers with SyncThru web interface. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/sensor.syncthru/ +""" + +import logging +import voluptuous as vol + +from homeassistant.const import ( + CONF_RESOURCE, CONF_HOST, CONF_NAME, CONF_MONITORED_CONDITIONS) +from homeassistant.helpers.entity import Entity +import homeassistant.helpers.config_validation as cv +from homeassistant.components.sensor import PLATFORM_SCHEMA + +REQUIREMENTS = ['pysyncthru==0.3.1'] + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_NAME = 'Samsung Printer' +DEFAULT_MONITORED_CONDITIONS = [ + 'toner_black', + 'toner_cyan', + 'toner_magenta', + 'toner_yellow', + 'drum_black', + 'drum_cyan', + 'drum_magenta', + 'drum_yellow', + 'tray_1', + 'tray_2', + 'tray_3', + 'tray_4', + 'tray_5', + 'output_tray_0', + 'output_tray_1', + 'output_tray_2', + 'output_tray_3', + 'output_tray_4', + 'output_tray_5', +] +COLORS = [ + 'black', + 'cyan', + 'magenta', + 'yellow' +] + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_RESOURCE): cv.url, + vol.Optional( + CONF_NAME, + default=DEFAULT_NAME + ): cv.string, + vol.Optional( + CONF_MONITORED_CONDITIONS, + default=DEFAULT_MONITORED_CONDITIONS + ): vol.All(cv.ensure_list, [vol.In(DEFAULT_MONITORED_CONDITIONS)]) +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the SyncThru component.""" + from pysyncthru import SyncThru, test_syncthru + + if discovery_info is not None: + host = discovery_info.get(CONF_HOST) + name = discovery_info.get(CONF_NAME, DEFAULT_NAME) + _LOGGER.debug("Discovered a new Samsung Printer: %s", discovery_info) + # Test if the discovered device actually is a syncthru printer + if not test_syncthru(host): + _LOGGER.error("No SyncThru Printer found at %s", host) + return + monitored = DEFAULT_MONITORED_CONDITIONS + else: + host = config.get(CONF_RESOURCE) + name = config.get(CONF_NAME) + monitored = config.get(CONF_MONITORED_CONDITIONS) + + # Main device, always added + try: + printer = SyncThru(host) + except TypeError: + # if an exception is thrown, printer cannot be set up + return + + printer.update() + devices = [SyncThruMainSensor(printer, name)] + + for key in printer.toner_status(filter_supported=True): + if 'toner_{}'.format(key) in monitored: + devices.append(SyncThruTonerSensor(printer, name, key)) + for key in printer.drum_status(filter_supported=True): + if 'drum_{}'.format(key) in monitored: + devices.append(SyncThruDrumSensor(printer, name, key)) + for key in printer.input_tray_status(filter_supported=True): + if 'tray_{}'.format(key) in monitored: + devices.append(SyncThruInputTraySensor(printer, name, key)) + for key in printer.output_tray_status(): + if 'output_tray_{}'.format(key) in monitored: + devices.append(SyncThruOutputTraySensor(printer, name, key)) + + add_devices(devices, True) + + +class SyncThruSensor(Entity): + """Implementation of an abstract Samsung Printer sensor platform.""" + + def __init__(self, syncthru, name): + """Initialize the sensor.""" + self.syncthru = syncthru + self._attributes = {} + self._state = None + self._name = name + self._icon = 'mdi:printer' + self._unit_of_measurement = None + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def state(self): + """Return the state of the device.""" + return self._state + + @property + def icon(self): + """Return the icon of the device.""" + return self._icon + + @property + def unit_of_measurement(self): + """Return the unit of measuremnt.""" + return self._unit_of_measurement + + @property + def device_state_attributes(self): + """Return the state attributes of the device.""" + return self._attributes + + +class SyncThruMainSensor(SyncThruSensor): + """Implementation of the main sensor, monitoring the general state.""" + + def update(self): + """Get the latest data from SyncThru and update the state.""" + self.syncthru.update() + self._state = self.syncthru.device_status() + + +class SyncThruTonerSensor(SyncThruSensor): + """Implementation of a Samsung Printer toner sensor platform.""" + + def __init__(self, syncthru, name, color): + """Initialize the sensor.""" + super().__init__(syncthru, name) + self._name = "{} Toner {}".format(name, color) + self._color = color + self._unit_of_measurement = '%' + + def update(self): + """Get the latest data from SyncThru and update the state.""" + # Data fetching is taken care of through the Main sensor + + if self.syncthru.is_online(): + self._attributes = self.syncthru.toner_status( + ).get(self._color, {}) + self._state = self._attributes.get('remaining') + + +class SyncThruDrumSensor(SyncThruSensor): + """Implementation of a Samsung Printer toner sensor platform.""" + + def __init__(self, syncthru, name, color): + """Initialize the sensor.""" + super().__init__(syncthru, name) + self._name = "{} Drum {}".format(name, color) + self._color = color + self._unit_of_measurement = '%' + + def update(self): + """Get the latest data from SyncThru and update the state.""" + # Data fetching is taken care of through the Main sensor + + if self.syncthru.is_online(): + self._attributes = self.syncthru.drum_status( + ).get(self._color, {}) + self._state = self._attributes.get('remaining') + + +class SyncThruInputTraySensor(SyncThruSensor): + """Implementation of a Samsung Printer input tray sensor platform.""" + + def __init__(self, syncthru, name, number): + """Initialize the sensor.""" + super().__init__(syncthru, name) + self._name = "{} Tray {}".format(name, number) + self._number = number + + def update(self): + """Get the latest data from SyncThru and update the state.""" + # Data fetching is taken care of through the Main sensor + + if self.syncthru.is_online(): + self._attributes = self.syncthru.input_tray_status( + ).get(self._number, {}) + self._state = self._attributes.get('newError') + if self._state == '': + self._state = 'Ready' + + +class SyncThruOutputTraySensor(SyncThruSensor): + """Implementation of a Samsung Printer input tray sensor platform.""" + + def __init__(self, syncthru, name, number): + """Initialize the sensor.""" + super().__init__(syncthru, name) + self._name = "{} Output Tray {}".format(name, number) + self._number = number + + def update(self): + """Get the latest data from SyncThru and update the state.""" + # Data fetching is taken care of through the Main sensor + + if self.syncthru.is_online(): + self._attributes = self.syncthru.output_tray_status( + ).get(self._number, {}) + self._state = self._attributes.get('status') + if self._state == '': + self._state = 'Ready' diff --git a/requirements_all.txt b/requirements_all.txt index 37c7150c146..91173a1825c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -894,6 +894,9 @@ pysnmp==4.4.4 # homeassistant.components.notify.stride pystride==0.1.7 +# homeassistant.components.sensor.syncthru +pysyncthru==0.3.1 + # homeassistant.components.media_player.liveboxplaytv pyteleloisirs==3.3 From 89c7c80e42c769857389a3edda2a874b63f974f5 Mon Sep 17 00:00:00 2001 From: Adam Mills Date: Sun, 18 Mar 2018 18:00:29 -0400 Subject: [PATCH 143/220] Use hue/sat as internal light color interface (#11288) * Accept and report both xy and RGB color for lights * Fix demo light supported_features * Add new XY color util functions * Always make color changes available as xy and RGB * Always expose color as RGB and XY * Consolidate color supported_features * Test fixes * Additional test fix * Use hue/sat as the hass core color interface * Tests updates * Assume MQTT RGB devices need full RGB brightness * Convert new platforms * More migration * Use float for HS API * Fix backwards conversion for KNX lights * Adjust limitless min saturation for new scale --- homeassistant/components/alexa/smart_home.py | 21 +---- .../components/google_assistant/trait.py | 15 ++- .../components/homekit/type_lights.py | 28 +++--- homeassistant/components/light/__init__.py | 54 ++++++----- homeassistant/components/light/abode.py | 19 ++-- .../components/light/blinksticklight.py | 44 ++++++--- homeassistant/components/light/blinkt.py | 27 +++--- homeassistant/components/light/deconz.py | 23 ++--- homeassistant/components/light/demo.py | 40 +++----- homeassistant/components/light/flux_led.py | 14 +-- homeassistant/components/light/group.py | 45 +++------ homeassistant/components/light/hive.py | 29 +++--- homeassistant/components/light/hue.py | 50 ++++------ homeassistant/components/light/hyperion.py | 17 ++-- homeassistant/components/light/iglo.py | 16 ++-- homeassistant/components/light/knx.py | 23 ++--- homeassistant/components/light/lifx.py | 54 +++++------ homeassistant/components/light/lifx_legacy.py | 47 ++-------- .../components/light/limitlessled.py | 29 +++--- homeassistant/components/light/mqtt.py | 91 +++++++++---------- homeassistant/components/light/mqtt_json.py | 85 ++++++++--------- .../components/light/mqtt_template.py | 43 +++++---- homeassistant/components/light/mysensors.py | 23 ++--- homeassistant/components/light/mystrom.py | 19 ++-- .../components/light/osramlightify.py | 61 ++++--------- homeassistant/components/light/piglow.py | 27 +++--- .../components/light/rpi_gpio_pwm.py | 18 ++-- homeassistant/components/light/sensehat.py | 27 +++--- homeassistant/components/light/skybell.py | 16 ++-- homeassistant/components/light/tikteck.py | 22 +++-- homeassistant/components/light/tplink.py | 45 +++------ homeassistant/components/light/tradfri.py | 29 +++--- homeassistant/components/light/vera.py | 17 ++-- homeassistant/components/light/wemo.py | 32 +++---- homeassistant/components/light/wink.py | 28 +++--- .../components/light/xiaomi_aqara.py | 24 ++--- homeassistant/components/light/yeelight.py | 71 ++++----------- .../components/light/yeelightsunflower.py | 21 +++-- homeassistant/components/light/zengge.py | 42 ++++++--- homeassistant/components/light/zha.py | 35 +++---- homeassistant/components/light/zwave.py | 73 ++++++++------- homeassistant/components/switch/flux.py | 4 +- homeassistant/util/color.py | 41 ++++++++- tests/components/alexa/test_smart_home.py | 36 -------- .../google_assistant/test_smart_home.py | 10 +- .../components/google_assistant/test_trait.py | 10 +- tests/components/homekit/test_type_lights.py | 14 +-- tests/components/light/test_demo.py | 11 ++- tests/components/light/test_group.py | 48 ++-------- tests/components/light/test_init.py | 51 ++++++----- tests/components/light/test_mqtt.py | 28 +++--- tests/components/light/test_mqtt_json.py | 17 ++-- tests/components/light/test_mqtt_template.py | 13 +-- tests/components/light/test_zwave.py | 62 ++++++++----- tests/testing_config/.remember_the_milk.conf | 1 + .../custom_components/light/test.py | 2 +- tests/util/test_color.py | 71 +++++++++++++-- 57 files changed, 898 insertions(+), 965 deletions(-) create mode 100644 tests/testing_config/.remember_the_milk.conf diff --git a/homeassistant/components/alexa/smart_home.py b/homeassistant/components/alexa/smart_home.py index 0d325534266..5e5155b3db8 100644 --- a/homeassistant/components/alexa/smart_home.py +++ b/homeassistant/components/alexa/smart_home.py @@ -438,9 +438,7 @@ class _LightCapabilities(_AlexaEntity): supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) if supported & light.SUPPORT_BRIGHTNESS: yield _AlexaBrightnessController(self.entity) - if supported & light.SUPPORT_RGB_COLOR: - yield _AlexaColorController(self.entity) - if supported & light.SUPPORT_XY_COLOR: + if supported & light.SUPPORT_COLOR: yield _AlexaColorController(self.entity) if supported & light.SUPPORT_COLOR_TEMP: yield _AlexaColorTemperatureController(self.entity) @@ -842,25 +840,16 @@ def async_api_adjust_brightness(hass, config, request, entity): @asyncio.coroutine def async_api_set_color(hass, config, request, entity): """Process a set color request.""" - supported = entity.attributes.get(ATTR_SUPPORTED_FEATURES) rgb = color_util.color_hsb_to_RGB( float(request[API_PAYLOAD]['color']['hue']), float(request[API_PAYLOAD]['color']['saturation']), float(request[API_PAYLOAD]['color']['brightness']) ) - if supported & light.SUPPORT_RGB_COLOR > 0: - yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, { - ATTR_ENTITY_ID: entity.entity_id, - light.ATTR_RGB_COLOR: rgb, - }, blocking=False) - else: - xyz = color_util.color_RGB_to_xy(*rgb) - yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, { - ATTR_ENTITY_ID: entity.entity_id, - light.ATTR_XY_COLOR: (xyz[0], xyz[1]), - light.ATTR_BRIGHTNESS: xyz[2], - }, blocking=False) + yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, { + ATTR_ENTITY_ID: entity.entity_id, + light.ATTR_RGB_COLOR: rgb, + }, blocking=False) return api_message(request) diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index c78d70e21e6..2f60f226042 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -243,7 +243,7 @@ class ColorSpectrumTrait(_Trait): if domain != light.DOMAIN: return False - return features & (light.SUPPORT_RGB_COLOR | light.SUPPORT_XY_COLOR) + return features & light.SUPPORT_COLOR def sync_attributes(self): """Return color spectrum attributes for a sync request.""" @@ -254,13 +254,11 @@ class ColorSpectrumTrait(_Trait): """Return color spectrum query attributes.""" response = {} - # No need to handle XY color because light component will always - # convert XY to RGB if possible (which is when brightness is available) - color_rgb = self.state.attributes.get(light.ATTR_RGB_COLOR) - if color_rgb is not None: + color_hs = self.state.attributes.get(light.ATTR_HS_COLOR) + if color_hs is not None: response['color'] = { 'spectrumRGB': int(color_util.color_rgb_to_hex( - color_rgb[0], color_rgb[1], color_rgb[2]), 16), + *color_util.color_hs_to_RGB(*color_hs)), 16), } return response @@ -274,11 +272,12 @@ class ColorSpectrumTrait(_Trait): """Execute a color spectrum command.""" # Convert integer to hex format and left pad with 0's till length 6 hex_value = "{0:06x}".format(params['color']['spectrumRGB']) - color = color_util.rgb_hex_to_rgb_list(hex_value) + color = color_util.color_RGB_to_hs( + *color_util.rgb_hex_to_rgb_list(hex_value)) await hass.services.async_call(light.DOMAIN, SERVICE_TURN_ON, { ATTR_ENTITY_ID: self.state.entity_id, - light.ATTR_RGB_COLOR: color + light.ATTR_HS_COLOR: color }, blocking=True) diff --git a/homeassistant/components/homekit/type_lights.py b/homeassistant/components/homekit/type_lights.py index db7172bef17..6cd60698110 100644 --- a/homeassistant/components/homekit/type_lights.py +++ b/homeassistant/components/homekit/type_lights.py @@ -2,10 +2,8 @@ import logging from homeassistant.components.light import ( - ATTR_RGB_COLOR, ATTR_BRIGHTNESS, - SUPPORT_BRIGHTNESS, SUPPORT_RGB_COLOR) + ATTR_HS_COLOR, ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, SUPPORT_COLOR) from homeassistant.const import ATTR_SUPPORTED_FEATURES, STATE_ON, STATE_OFF -from homeassistant.util.color import color_RGB_to_hsv, color_hsv_to_RGB from . import TYPES from .accessories import HomeAccessory, add_preload_service @@ -40,7 +38,7 @@ class Light(HomeAccessory): .attributes.get(ATTR_SUPPORTED_FEATURES) if self._features & SUPPORT_BRIGHTNESS: self.chars.append(CHAR_BRIGHTNESS) - if self._features & SUPPORT_RGB_COLOR: + if self._features & SUPPORT_COLOR: self.chars.append(CHAR_HUE) self.chars.append(CHAR_SATURATION) self._hue = None @@ -102,15 +100,15 @@ class Light(HomeAccessory): def set_color(self): """Set color if call came from HomeKit.""" - # Handle RGB Color - if self._features & SUPPORT_RGB_COLOR and self._flag[CHAR_HUE] and \ + # Handle Color + if self._features & SUPPORT_COLOR and self._flag[CHAR_HUE] and \ self._flag[CHAR_SATURATION]: - color = color_hsv_to_RGB(self._hue, self._saturation, 100) - _LOGGER.debug('%s: Set rgb_color to %s', self._entity_id, color) + color = (self._hue, self._saturation) + _LOGGER.debug('%s: Set hs_color to %s', self._entity_id, color) self._flag.update({ CHAR_HUE: False, CHAR_SATURATION: False, RGB_COLOR: True}) self._hass.components.light.turn_on( - self._entity_id, rgb_color=color) + self._entity_id, hs_color=color) def update_state(self, entity_id=None, old_state=None, new_state=None): """Update light after state change.""" @@ -134,15 +132,11 @@ class Light(HomeAccessory): should_callback=False) self._flag[CHAR_BRIGHTNESS] = False - # Handle RGB Color + # Handle Color if CHAR_SATURATION in self.chars and CHAR_HUE in self.chars: - rgb_color = new_state.attributes.get(ATTR_RGB_COLOR) - current_color = color_hsv_to_RGB(self._hue, self._saturation, 100)\ - if self._hue and self._saturation else [None] * 3 - if not self._flag[RGB_COLOR] and \ - isinstance(rgb_color, (list, tuple)) and \ - tuple(rgb_color) != current_color: - hue, saturation, _ = color_RGB_to_hsv(*rgb_color) + hue, saturation = new_state.attributes.get(ATTR_HS_COLOR) + if not self._flag[RGB_COLOR] and ( + hue != self._hue or saturation != self._saturation): self.char_hue.set_value(hue, should_callback=False) self.char_saturation.set_value(saturation, should_callback=False) diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index a3a962a7e34..f03521947b7 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -40,9 +40,8 @@ SUPPORT_BRIGHTNESS = 1 SUPPORT_COLOR_TEMP = 2 SUPPORT_EFFECT = 4 SUPPORT_FLASH = 8 -SUPPORT_RGB_COLOR = 16 +SUPPORT_COLOR = 16 SUPPORT_TRANSITION = 32 -SUPPORT_XY_COLOR = 64 SUPPORT_WHITE_VALUE = 128 # Integer that represents transition time in seconds to make change. @@ -51,6 +50,7 @@ ATTR_TRANSITION = "transition" # Lists holding color values ATTR_RGB_COLOR = "rgb_color" ATTR_XY_COLOR = "xy_color" +ATTR_HS_COLOR = "hs_color" ATTR_COLOR_TEMP = "color_temp" ATTR_KELVIN = "kelvin" ATTR_MIN_MIREDS = "min_mireds" @@ -86,8 +86,9 @@ LIGHT_PROFILES_FILE = "light_profiles.csv" PROP_TO_ATTR = { 'brightness': ATTR_BRIGHTNESS, 'color_temp': ATTR_COLOR_TEMP, - 'rgb_color': ATTR_RGB_COLOR, - 'xy_color': ATTR_XY_COLOR, + 'min_mireds': ATTR_MIN_MIREDS, + 'max_mireds': ATTR_MAX_MIREDS, + 'hs_color': ATTR_HS_COLOR, 'white_value': ATTR_WHITE_VALUE, 'effect_list': ATTR_EFFECT_LIST, 'effect': ATTR_EFFECT, @@ -111,6 +112,11 @@ LIGHT_TURN_ON_SCHEMA = vol.Schema({ vol.Exclusive(ATTR_XY_COLOR, COLOR_GROUP): vol.All(vol.ExactSequence((cv.small_float, cv.small_float)), vol.Coerce(tuple)), + vol.Exclusive(ATTR_HS_COLOR, COLOR_GROUP): + vol.All(vol.ExactSequence( + (vol.All(vol.Coerce(float), vol.Range(min=0, max=360)), + vol.All(vol.Coerce(float), vol.Range(min=0, max=100)))), + vol.Coerce(tuple)), vol.Exclusive(ATTR_COLOR_TEMP, COLOR_GROUP): vol.All(vol.Coerce(int), vol.Range(min=1)), vol.Exclusive(ATTR_KELVIN, COLOR_GROUP): @@ -149,13 +155,13 @@ def is_on(hass, entity_id=None): @bind_hass def turn_on(hass, entity_id=None, transition=None, brightness=None, - brightness_pct=None, rgb_color=None, xy_color=None, + brightness_pct=None, rgb_color=None, xy_color=None, hs_color=None, color_temp=None, kelvin=None, white_value=None, profile=None, flash=None, effect=None, color_name=None): """Turn all or specified light on.""" hass.add_job( async_turn_on, hass, entity_id, transition, brightness, brightness_pct, - rgb_color, xy_color, color_temp, kelvin, white_value, + rgb_color, xy_color, hs_color, color_temp, kelvin, white_value, profile, flash, effect, color_name) @@ -163,8 +169,9 @@ def turn_on(hass, entity_id=None, transition=None, brightness=None, @bind_hass def async_turn_on(hass, entity_id=None, transition=None, brightness=None, brightness_pct=None, rgb_color=None, xy_color=None, - color_temp=None, kelvin=None, white_value=None, - profile=None, flash=None, effect=None, color_name=None): + hs_color=None, color_temp=None, kelvin=None, + white_value=None, profile=None, flash=None, effect=None, + color_name=None): """Turn all or specified light on.""" data = { key: value for key, value in [ @@ -175,6 +182,7 @@ def async_turn_on(hass, entity_id=None, transition=None, brightness=None, (ATTR_BRIGHTNESS_PCT, brightness_pct), (ATTR_RGB_COLOR, rgb_color), (ATTR_XY_COLOR, xy_color), + (ATTR_HS_COLOR, hs_color), (ATTR_COLOR_TEMP, color_temp), (ATTR_KELVIN, kelvin), (ATTR_WHITE_VALUE, white_value), @@ -254,6 +262,14 @@ def preprocess_turn_on_alternatives(params): if brightness_pct is not None: params[ATTR_BRIGHTNESS] = int(255 * brightness_pct/100) + xy_color = params.pop(ATTR_XY_COLOR, None) + if xy_color is not None: + params[ATTR_HS_COLOR] = color_util.color_xy_to_hs(*xy_color) + + rgb_color = params.pop(ATTR_RGB_COLOR, None) + if rgb_color is not None: + params[ATTR_HS_COLOR] = color_util.color_RGB_to_hs(*rgb_color) + class SetIntentHandler(intent.IntentHandler): """Handle set color intents.""" @@ -281,7 +297,7 @@ class SetIntentHandler(intent.IntentHandler): if 'color' in slots: intent.async_test_feature( - state, SUPPORT_RGB_COLOR, 'changing colors') + state, SUPPORT_COLOR, 'changing colors') service_data[ATTR_RGB_COLOR] = slots['color']['value'] # Use original passed in value of the color because we don't have # human readable names for that internally. @@ -428,13 +444,8 @@ class Light(ToggleEntity): return None @property - def xy_color(self): - """Return the XY color value [float, float].""" - return None - - @property - def rgb_color(self): - """Return the RGB color value [int, int, int].""" + def hs_color(self): + """Return the hue and saturation color value [float, float].""" return None @property @@ -484,11 +495,12 @@ class Light(ToggleEntity): if value is not None: data[attr] = value - if ATTR_RGB_COLOR not in data and ATTR_XY_COLOR in data and \ - ATTR_BRIGHTNESS in data: - data[ATTR_RGB_COLOR] = color_util.color_xy_brightness_to_RGB( - data[ATTR_XY_COLOR][0], data[ATTR_XY_COLOR][1], - data[ATTR_BRIGHTNESS]) + # Expose current color also as RGB and XY + if ATTR_HS_COLOR in data: + data[ATTR_RGB_COLOR] = color_util.color_hs_to_RGB( + *data[ATTR_HS_COLOR]) + data[ATTR_XY_COLOR] = color_util.color_hs_to_xy( + *data[ATTR_HS_COLOR]) return data diff --git a/homeassistant/components/light/abode.py b/homeassistant/components/light/abode.py index d3e79b38647..bfea19fc3fa 100644 --- a/homeassistant/components/light/abode.py +++ b/homeassistant/components/light/abode.py @@ -8,8 +8,9 @@ import logging from homeassistant.components.abode import AbodeDevice, DOMAIN as ABODE_DOMAIN from homeassistant.components.light import ( - ATTR_BRIGHTNESS, ATTR_RGB_COLOR, - SUPPORT_BRIGHTNESS, SUPPORT_RGB_COLOR, Light) + ATTR_BRIGHTNESS, ATTR_HS_COLOR, + SUPPORT_BRIGHTNESS, SUPPORT_COLOR, Light) +import homeassistant.util.color as color_util DEPENDENCIES = ['abode'] @@ -44,10 +45,12 @@ class AbodeLight(AbodeDevice, Light): def turn_on(self, **kwargs): """Turn on the light.""" - if (ATTR_RGB_COLOR in kwargs and + if (ATTR_HS_COLOR in kwargs and self._device.is_dimmable and self._device.has_color): - self._device.set_color(kwargs[ATTR_RGB_COLOR]) - elif ATTR_BRIGHTNESS in kwargs and self._device.is_dimmable: + self._device.set_color(color_util.color_hs_to_RGB( + *kwargs[ATTR_HS_COLOR])) + + if ATTR_BRIGHTNESS in kwargs and self._device.is_dimmable: self._device.set_level(kwargs[ATTR_BRIGHTNESS]) else: self._device.switch_on() @@ -68,16 +71,16 @@ class AbodeLight(AbodeDevice, Light): return self._device.brightness @property - def rgb_color(self): + def hs_color(self): """Return the color of the light.""" if self._device.is_dimmable and self._device.has_color: - return self._device.color + return color_util.color_RGB_to_hs(*self._device.color) @property def supported_features(self): """Flag supported features.""" if self._device.is_dimmable and self._device.has_color: - return SUPPORT_BRIGHTNESS | SUPPORT_RGB_COLOR + return SUPPORT_BRIGHTNESS | SUPPORT_COLOR elif self._device.is_dimmable: return SUPPORT_BRIGHTNESS diff --git a/homeassistant/components/light/blinksticklight.py b/homeassistant/components/light/blinksticklight.py index d6a6ef465a8..18a6b4ae266 100644 --- a/homeassistant/components/light/blinksticklight.py +++ b/homeassistant/components/light/blinksticklight.py @@ -9,9 +9,11 @@ import logging import voluptuous as vol from homeassistant.components.light import ( - ATTR_RGB_COLOR, SUPPORT_RGB_COLOR, Light, PLATFORM_SCHEMA) + ATTR_BRIGHTNESS, ATTR_HS_COLOR, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, Light, + PLATFORM_SCHEMA) from homeassistant.const import CONF_NAME import homeassistant.helpers.config_validation as cv +import homeassistant.util.color as color_util REQUIREMENTS = ['blinkstick==1.1.8'] @@ -21,7 +23,7 @@ CONF_SERIAL = 'serial' DEFAULT_NAME = 'Blinkstick' -SUPPORT_BLINKSTICK = SUPPORT_RGB_COLOR +SUPPORT_BLINKSTICK = SUPPORT_BRIGHTNESS | SUPPORT_COLOR PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_SERIAL): cv.string, @@ -39,7 +41,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): stick = blinkstick.find_by_serial(serial) - add_devices([BlinkStickLight(stick, name)]) + add_devices([BlinkStickLight(stick, name)], True) class BlinkStickLight(Light): @@ -50,7 +52,8 @@ class BlinkStickLight(Light): self._stick = stick self._name = name self._serial = stick.get_serial() - self._rgb_color = stick.get_color() + self._hs_color = None + self._brightness = None @property def should_poll(self): @@ -63,14 +66,19 @@ class BlinkStickLight(Light): return self._name @property - def rgb_color(self): + def brightness(self): + """Read back the brightness of the light.""" + return self._brightness + + @property + def hs_color(self): """Read back the color of the light.""" - return self._rgb_color + return self._hs_color @property def is_on(self): - """Check whether any of the LEDs colors are non-zero.""" - return sum(self._rgb_color) > 0 + """Return True if entity is on.""" + return self._brightness > 0 @property def supported_features(self): @@ -79,18 +87,24 @@ class BlinkStickLight(Light): def update(self): """Read back the device state.""" - self._rgb_color = self._stick.get_color() + rgb_color = self._stick.get_color() + hsv = color_util.color_RGB_to_hsv(*rgb_color) + self._hs_color = hsv[:2] + self._brightness = hsv[2] def turn_on(self, **kwargs): """Turn the device on.""" - if ATTR_RGB_COLOR in kwargs: - self._rgb_color = kwargs[ATTR_RGB_COLOR] + if ATTR_HS_COLOR in kwargs: + self._hs_color = kwargs[ATTR_HS_COLOR] + if ATTR_BRIGHTNESS in kwargs: + self._brightness = kwargs[ATTR_BRIGHTNESS] else: - self._rgb_color = [255, 255, 255] + self._brightness = 255 - self._stick.set_color(red=self._rgb_color[0], - green=self._rgb_color[1], - blue=self._rgb_color[2]) + rgb_color = color_util.color_hsv_to_RGB( + self._hs_color[0], self._hs_color[1], self._brightness / 255 * 100) + self._stick.set_color( + red=rgb_color[0], green=rgb_color[1], blue=rgb_color[2]) def turn_off(self, **kwargs): """Turn the device off.""" diff --git a/homeassistant/components/light/blinkt.py b/homeassistant/components/light/blinkt.py index db3171cf4cf..97edd7c54d2 100644 --- a/homeassistant/components/light/blinkt.py +++ b/homeassistant/components/light/blinkt.py @@ -10,15 +10,16 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.components.light import ( - ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, ATTR_RGB_COLOR, SUPPORT_RGB_COLOR, + ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, ATTR_HS_COLOR, SUPPORT_COLOR, Light, PLATFORM_SCHEMA) from homeassistant.const import CONF_NAME +import homeassistant.util.color as color_util REQUIREMENTS = ['blinkt==0.1.0'] _LOGGER = logging.getLogger(__name__) -SUPPORT_BLINKT = (SUPPORT_BRIGHTNESS | SUPPORT_RGB_COLOR) +SUPPORT_BLINKT = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR) DEFAULT_NAME = 'blinkt' @@ -55,7 +56,7 @@ class BlinktLight(Light): self._index = index self._is_on = False self._brightness = 255 - self._rgb_color = [255, 255, 255] + self._hs_color = [0, 0] @property def name(self): @@ -71,12 +72,9 @@ class BlinktLight(Light): return self._brightness @property - def rgb_color(self): - """Read back the color of the light. - - Returns [r, g, b] list with values in range of 0-255. - """ - return self._rgb_color + def hs_color(self): + """Read back the color of the light.""" + return self._hs_color @property def supported_features(self): @@ -100,16 +98,17 @@ class BlinktLight(Light): def turn_on(self, **kwargs): """Instruct the light to turn on and set correct brightness & color.""" - if ATTR_RGB_COLOR in kwargs: - self._rgb_color = kwargs[ATTR_RGB_COLOR] + if ATTR_HS_COLOR in kwargs: + self._hs_color = kwargs[ATTR_HS_COLOR] if ATTR_BRIGHTNESS in kwargs: self._brightness = kwargs[ATTR_BRIGHTNESS] percent_bright = (self._brightness / 255) + rgb_color = color_util.color_hs_to_RGB(*self._hs_color) self._blinkt.set_pixel(self._index, - self._rgb_color[0], - self._rgb_color[1], - self._rgb_color[2], + rgb_color[0], + rgb_color[1], + rgb_color[2], percent_bright) self._blinkt.show() diff --git a/homeassistant/components/light/deconz.py b/homeassistant/components/light/deconz.py index a3e54434109..020f43d9935 100644 --- a/homeassistant/components/light/deconz.py +++ b/homeassistant/components/light/deconz.py @@ -7,12 +7,12 @@ https://home-assistant.io/components/light.deconz/ from homeassistant.components.deconz import ( DOMAIN as DATA_DECONZ, DATA_DECONZ_ID) from homeassistant.components.light import ( - ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_FLASH, ATTR_RGB_COLOR, - ATTR_TRANSITION, ATTR_XY_COLOR, EFFECT_COLORLOOP, FLASH_LONG, FLASH_SHORT, - SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, SUPPORT_FLASH, - SUPPORT_RGB_COLOR, SUPPORT_TRANSITION, SUPPORT_XY_COLOR, Light) + ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_FLASH, ATTR_HS_COLOR, + ATTR_TRANSITION, EFFECT_COLORLOOP, FLASH_LONG, FLASH_SHORT, + SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, + SUPPORT_FLASH, SUPPORT_TRANSITION, Light) from homeassistant.core import callback -from homeassistant.util.color import color_RGB_to_xy +import homeassistant.util.color as color_util DEPENDENCIES = ['deconz'] @@ -51,8 +51,7 @@ class DeconzLight(Light): self._features |= SUPPORT_COLOR_TEMP if self._light.xy is not None: - self._features |= SUPPORT_RGB_COLOR - self._features |= SUPPORT_XY_COLOR + self._features |= SUPPORT_COLOR if self._light.effect is not None: self._features |= SUPPORT_EFFECT @@ -124,14 +123,8 @@ class DeconzLight(Light): if ATTR_COLOR_TEMP in kwargs: data['ct'] = kwargs[ATTR_COLOR_TEMP] - if ATTR_RGB_COLOR in kwargs: - xyb = color_RGB_to_xy( - *(int(val) for val in kwargs[ATTR_RGB_COLOR])) - data['xy'] = xyb[0], xyb[1] - data['bri'] = xyb[2] - - if ATTR_XY_COLOR in kwargs: - data['xy'] = kwargs[ATTR_XY_COLOR] + if ATTR_HS_COLOR in kwargs: + data['xy'] = color_util.color_hs_to_xy(*kwargs[ATTR_HS_COLOR]) if ATTR_BRIGHTNESS in kwargs: data['bri'] = kwargs[ATTR_BRIGHTNESS] diff --git a/homeassistant/components/light/demo.py b/homeassistant/components/light/demo.py index acc70a57ff4..05aecd542e2 100644 --- a/homeassistant/components/light/demo.py +++ b/homeassistant/components/light/demo.py @@ -7,14 +7,13 @@ https://home-assistant.io/components/demo/ import random from homeassistant.components.light import ( - ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, - ATTR_RGB_COLOR, ATTR_WHITE_VALUE, ATTR_XY_COLOR, SUPPORT_BRIGHTNESS, - SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, SUPPORT_RGB_COLOR, SUPPORT_WHITE_VALUE, - Light) + ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_HS_COLOR, + ATTR_WHITE_VALUE, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, + SUPPORT_COLOR, SUPPORT_WHITE_VALUE, Light) LIGHT_COLORS = [ - [237, 224, 33], - [255, 63, 111], + (56, 86), + (345, 75), ] LIGHT_EFFECT_LIST = ['rainbow', 'none'] @@ -22,7 +21,7 @@ LIGHT_EFFECT_LIST = ['rainbow', 'none'] LIGHT_TEMPS = [240, 380] SUPPORT_DEMO = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_EFFECT | - SUPPORT_RGB_COLOR | SUPPORT_WHITE_VALUE) + SUPPORT_COLOR | SUPPORT_WHITE_VALUE) def setup_platform(hass, config, add_devices_callback, discovery_info=None): @@ -40,17 +39,16 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): class DemoLight(Light): """Representation of a demo light.""" - def __init__(self, unique_id, name, state, available=False, rgb=None, - ct=None, brightness=180, xy_color=(.5, .5), white=200, - effect_list=None, effect=None): + def __init__(self, unique_id, name, state, available=False, hs_color=None, + ct=None, brightness=180, white=200, effect_list=None, + effect=None): """Initialize the light.""" self._unique_id = unique_id self._name = name self._state = state - self._rgb = rgb + self._hs_color = hs_color self._ct = ct or random.choice(LIGHT_TEMPS) self._brightness = brightness - self._xy_color = xy_color self._white = white self._effect_list = effect_list self._effect = effect @@ -83,14 +81,9 @@ class DemoLight(Light): return self._brightness @property - def xy_color(self) -> tuple: - """Return the XY color value [float, float].""" - return self._xy_color - - @property - def rgb_color(self) -> tuple: - """Return the RBG color value.""" - return self._rgb + def hs_color(self) -> tuple: + """Return the hs color value.""" + return self._hs_color @property def color_temp(self) -> int: @@ -126,8 +119,8 @@ class DemoLight(Light): """Turn the light on.""" self._state = True - if ATTR_RGB_COLOR in kwargs: - self._rgb = kwargs[ATTR_RGB_COLOR] + if ATTR_HS_COLOR in kwargs: + self._hs_color = kwargs[ATTR_HS_COLOR] if ATTR_COLOR_TEMP in kwargs: self._ct = kwargs[ATTR_COLOR_TEMP] @@ -135,9 +128,6 @@ class DemoLight(Light): if ATTR_BRIGHTNESS in kwargs: self._brightness = kwargs[ATTR_BRIGHTNESS] - if ATTR_XY_COLOR in kwargs: - self._xy_color = kwargs[ATTR_XY_COLOR] - if ATTR_WHITE_VALUE in kwargs: self._white = kwargs[ATTR_WHITE_VALUE] diff --git a/homeassistant/components/light/flux_led.py b/homeassistant/components/light/flux_led.py index 2a239c9ae10..ed0836f1449 100644 --- a/homeassistant/components/light/flux_led.py +++ b/homeassistant/components/light/flux_led.py @@ -12,10 +12,11 @@ import voluptuous as vol from homeassistant.const import CONF_DEVICES, CONF_NAME, CONF_PROTOCOL from homeassistant.components.light import ( - ATTR_BRIGHTNESS, ATTR_RGB_COLOR, ATTR_EFFECT, EFFECT_COLORLOOP, + ATTR_BRIGHTNESS, ATTR_HS_COLOR, ATTR_EFFECT, EFFECT_COLORLOOP, EFFECT_RANDOM, SUPPORT_BRIGHTNESS, SUPPORT_EFFECT, - SUPPORT_RGB_COLOR, Light, PLATFORM_SCHEMA) + SUPPORT_COLOR, Light, PLATFORM_SCHEMA) import homeassistant.helpers.config_validation as cv +import homeassistant.util.color as color_util REQUIREMENTS = ['flux_led==0.21'] @@ -27,7 +28,7 @@ ATTR_MODE = 'mode' DOMAIN = 'flux_led' SUPPORT_FLUX_LED = (SUPPORT_BRIGHTNESS | SUPPORT_EFFECT | - SUPPORT_RGB_COLOR) + SUPPORT_COLOR) MODE_RGB = 'rgb' MODE_RGBW = 'rgbw' @@ -183,9 +184,9 @@ class FluxLight(Light): return self._bulb.brightness @property - def rgb_color(self): + def hs_color(self): """Return the color property.""" - return self._bulb.getRgb() + return color_util.color_RGB_to_hs(*self._bulb.getRgb()) @property def supported_features(self): @@ -202,7 +203,8 @@ class FluxLight(Light): if not self.is_on: self._bulb.turnOn() - rgb = kwargs.get(ATTR_RGB_COLOR) + hs_color = kwargs.get(ATTR_HS_COLOR) + rgb = color_util.color_hs_to_RGB(*hs_color) brightness = kwargs.get(ATTR_BRIGHTNESS) effect = kwargs.get(ATTR_EFFECT) diff --git a/homeassistant/components/light/group.py b/homeassistant/components/light/group.py index b4a5e9dddfb..f9ffbb4e0bf 100644 --- a/homeassistant/components/light/group.py +++ b/homeassistant/components/light/group.py @@ -19,12 +19,11 @@ from homeassistant.const import (STATE_ON, ATTR_ENTITY_ID, CONF_NAME, from homeassistant.helpers.event import async_track_state_change from homeassistant.helpers.typing import HomeAssistantType, ConfigType from homeassistant.components.light import ( - SUPPORT_BRIGHTNESS, SUPPORT_RGB_COLOR, SUPPORT_COLOR_TEMP, - SUPPORT_TRANSITION, SUPPORT_EFFECT, SUPPORT_FLASH, SUPPORT_XY_COLOR, - SUPPORT_WHITE_VALUE, PLATFORM_SCHEMA, ATTR_BRIGHTNESS, ATTR_XY_COLOR, - ATTR_RGB_COLOR, ATTR_WHITE_VALUE, ATTR_COLOR_TEMP, ATTR_MIN_MIREDS, - ATTR_MAX_MIREDS, ATTR_EFFECT_LIST, ATTR_EFFECT, ATTR_FLASH, - ATTR_TRANSITION) + SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP, SUPPORT_TRANSITION, + SUPPORT_EFFECT, SUPPORT_FLASH, SUPPORT_WHITE_VALUE, PLATFORM_SCHEMA, + ATTR_BRIGHTNESS, ATTR_HS_COLOR, ATTR_WHITE_VALUE, ATTR_COLOR_TEMP, + ATTR_MIN_MIREDS, ATTR_MAX_MIREDS, ATTR_EFFECT_LIST, ATTR_EFFECT, + ATTR_FLASH, ATTR_TRANSITION) import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -37,8 +36,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) SUPPORT_GROUP_LIGHT = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_EFFECT - | SUPPORT_FLASH | SUPPORT_RGB_COLOR | SUPPORT_TRANSITION - | SUPPORT_XY_COLOR | SUPPORT_WHITE_VALUE) + | SUPPORT_FLASH | SUPPORT_COLOR | SUPPORT_TRANSITION + | SUPPORT_WHITE_VALUE) async def async_setup_platform(hass: HomeAssistantType, config: ConfigType, @@ -58,8 +57,7 @@ class LightGroup(light.Light): self._is_on = False # type: bool self._available = False # type: bool self._brightness = None # type: Optional[int] - self._xy_color = None # type: Optional[Tuple[float, float]] - self._rgb_color = None # type: Optional[Tuple[int, int, int]] + self._hs_color = None # type: Optional[Tuple[float, float]] self._color_temp = None # type: Optional[int] self._min_mireds = 154 # type: Optional[int] self._max_mireds = 500 # type: Optional[int] @@ -108,14 +106,9 @@ class LightGroup(light.Light): return self._brightness @property - def xy_color(self) -> Optional[Tuple[float, float]]: - """Return the XY color value [float, float].""" - return self._xy_color - - @property - def rgb_color(self) -> Optional[Tuple[int, int, int]]: - """Return the RGB color value [int, int, int].""" - return self._rgb_color + def hs_color(self) -> Optional[Tuple[float, float]]: + """Return the HS color value [float, float].""" + return self._hs_color @property def color_temp(self) -> Optional[int]: @@ -164,11 +157,8 @@ class LightGroup(light.Light): if ATTR_BRIGHTNESS in kwargs: data[ATTR_BRIGHTNESS] = kwargs[ATTR_BRIGHTNESS] - if ATTR_XY_COLOR in kwargs: - data[ATTR_XY_COLOR] = kwargs[ATTR_XY_COLOR] - - if ATTR_RGB_COLOR in kwargs: - data[ATTR_RGB_COLOR] = kwargs[ATTR_RGB_COLOR] + if ATTR_HS_COLOR in kwargs: + data[ATTR_HS_COLOR] = kwargs[ATTR_HS_COLOR] if ATTR_COLOR_TEMP in kwargs: data[ATTR_COLOR_TEMP] = kwargs[ATTR_COLOR_TEMP] @@ -210,13 +200,8 @@ class LightGroup(light.Light): self._brightness = _reduce_attribute(on_states, ATTR_BRIGHTNESS) - self._xy_color = _reduce_attribute( - on_states, ATTR_XY_COLOR, reduce=_mean_tuple) - - self._rgb_color = _reduce_attribute( - on_states, ATTR_RGB_COLOR, reduce=_mean_tuple) - if self._rgb_color is not None: - self._rgb_color = tuple(map(int, self._rgb_color)) + self._hs_color = _reduce_attribute( + on_states, ATTR_HS_COLOR, reduce=_mean_tuple) self._white_value = _reduce_attribute(on_states, ATTR_WHITE_VALUE) diff --git a/homeassistant/components/light/hive.py b/homeassistant/components/light/hive.py index e57bdf2c046..c4ecc5a9d2c 100644 --- a/homeassistant/components/light/hive.py +++ b/homeassistant/components/light/hive.py @@ -4,13 +4,13 @@ Support for the Hive devices. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/light.hive/ """ -import colorsys from homeassistant.components.hive import DATA_HIVE from homeassistant.components.light import (ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, - ATTR_RGB_COLOR, + ATTR_HS_COLOR, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, - SUPPORT_RGB_COLOR, Light) + SUPPORT_COLOR, Light) +import homeassistant.util.color as color_util DEPENDENCIES = ['hive'] @@ -75,10 +75,11 @@ class HiveDeviceLight(Light): return self.session.light.get_color_temp(self.node_id) @property - def rgb_color(self) -> tuple: - """Return the RBG color value.""" + def hs_color(self) -> tuple: + """Return the hs color value.""" if self.light_device_type == "colourtuneablelight": - return self.session.light.get_color(self.node_id) + rgb = self.session.light.get_color(self.node_id) + return color_util.color_RGB_to_hs(*rgb) @property def is_on(self): @@ -99,15 +100,11 @@ class HiveDeviceLight(Light): if ATTR_COLOR_TEMP in kwargs: tmp_new_color_temp = kwargs.get(ATTR_COLOR_TEMP) new_color_temp = round(1000000 / tmp_new_color_temp) - if ATTR_RGB_COLOR in kwargs: - get_new_color = kwargs.get(ATTR_RGB_COLOR) - tmp_new_color = colorsys.rgb_to_hsv(get_new_color[0], - get_new_color[1], - get_new_color[2]) - hue = int(round(tmp_new_color[0] * 360)) - saturation = int(round(tmp_new_color[1] * 100)) - value = int(round((tmp_new_color[2] / 255) * 100)) - new_color = (hue, saturation, value) + if ATTR_HS_COLOR in kwargs: + get_new_color = kwargs.get(ATTR_HS_COLOR) + hue = int(get_new_color[0]) + saturation = int(get_new_color[1]) + new_color = (hue, saturation, 100) self.session.light.turn_on(self.node_id, self.light_device_type, new_brightness, new_color_temp, @@ -132,7 +129,7 @@ class HiveDeviceLight(Light): supported_features = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP) elif self.light_device_type == "colourtuneablelight": supported_features = ( - SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_RGB_COLOR) + SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_COLOR) return supported_features diff --git a/homeassistant/components/light/hue.py b/homeassistant/components/light/hue.py index c45d9c5c44e..b1562aaba8f 100644 --- a/homeassistant/components/light/hue.py +++ b/homeassistant/components/light/hue.py @@ -13,12 +13,11 @@ import async_timeout import homeassistant.components.hue as hue from homeassistant.components.light import ( - ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_FLASH, ATTR_RGB_COLOR, - ATTR_TRANSITION, ATTR_XY_COLOR, EFFECT_COLORLOOP, EFFECT_RANDOM, - FLASH_LONG, FLASH_SHORT, SUPPORT_BRIGHTNESS, - SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, SUPPORT_FLASH, SUPPORT_RGB_COLOR, - SUPPORT_TRANSITION, SUPPORT_XY_COLOR, Light) -import homeassistant.util.color as color_util + ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_FLASH, + ATTR_TRANSITION, ATTR_HS_COLOR, EFFECT_COLORLOOP, EFFECT_RANDOM, + FLASH_LONG, FLASH_SHORT, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, + SUPPORT_EFFECT, SUPPORT_FLASH, SUPPORT_COLOR, SUPPORT_TRANSITION, + Light) DEPENDENCIES = ['hue'] SCAN_INTERVAL = timedelta(seconds=5) @@ -28,8 +27,7 @@ _LOGGER = logging.getLogger(__name__) SUPPORT_HUE_ON_OFF = (SUPPORT_FLASH | SUPPORT_TRANSITION) SUPPORT_HUE_DIMMABLE = (SUPPORT_HUE_ON_OFF | SUPPORT_BRIGHTNESS) SUPPORT_HUE_COLOR_TEMP = (SUPPORT_HUE_DIMMABLE | SUPPORT_COLOR_TEMP) -SUPPORT_HUE_COLOR = (SUPPORT_HUE_DIMMABLE | SUPPORT_EFFECT | - SUPPORT_RGB_COLOR | SUPPORT_XY_COLOR) +SUPPORT_HUE_COLOR = (SUPPORT_HUE_DIMMABLE | SUPPORT_EFFECT | SUPPORT_COLOR) SUPPORT_HUE_EXTENDED = (SUPPORT_HUE_COLOR_TEMP | SUPPORT_HUE_COLOR) SUPPORT_HUE = { @@ -228,11 +226,17 @@ class HueLight(Light): return self.light.state.get('bri') @property - def xy_color(self): - """Return the XY color value.""" + def hs_color(self): + """Return the hs color value.""" if self.is_group: - return self.light.action.get('xy') - return self.light.state.get('xy') + return ( + self.light.action.get('hue') / 65535 * 360, + self.light.action.get('sat') / 255 * 100, + ) + return ( + self.light.state.get('hue') / 65535 * 360, + self.light.state.get('sat') / 255 * 100, + ) @property def color_temp(self): @@ -272,25 +276,9 @@ class HueLight(Light): if ATTR_TRANSITION in kwargs: command['transitiontime'] = int(kwargs[ATTR_TRANSITION] * 10) - if ATTR_XY_COLOR in kwargs: - if self.is_osram: - color_hue, sat = color_util.color_xy_to_hs( - *kwargs[ATTR_XY_COLOR]) - command['hue'] = color_hue / 360 * 65535 - command['sat'] = sat / 100 * 255 - else: - command['xy'] = kwargs[ATTR_XY_COLOR] - elif ATTR_RGB_COLOR in kwargs: - if self.is_osram: - hsv = color_util.color_RGB_to_hsv( - *(int(val) for val in kwargs[ATTR_RGB_COLOR])) - command['hue'] = hsv[0] / 360 * 65535 - command['sat'] = hsv[1] / 100 * 255 - command['bri'] = hsv[2] / 100 * 255 - else: - xyb = color_util.color_RGB_to_xy( - *(int(val) for val in kwargs[ATTR_RGB_COLOR])) - command['xy'] = xyb[0], xyb[1] + if ATTR_HS_COLOR in kwargs: + command['hue'] = int(kwargs[ATTR_HS_COLOR][0] / 360 * 65535) + command['sat'] = int(kwargs[ATTR_HS_COLOR][1] / 100 * 255) elif ATTR_COLOR_TEMP in kwargs: temp = kwargs[ATTR_COLOR_TEMP] command['ct'] = max(self.min_mireds, min(temp, self.max_mireds)) diff --git a/homeassistant/components/light/hyperion.py b/homeassistant/components/light/hyperion.py index 2057192299e..e5a4bd18115 100644 --- a/homeassistant/components/light/hyperion.py +++ b/homeassistant/components/light/hyperion.py @@ -11,10 +11,11 @@ import socket import voluptuous as vol from homeassistant.components.light import ( - ATTR_BRIGHTNESS, ATTR_RGB_COLOR, ATTR_EFFECT, SUPPORT_BRIGHTNESS, - SUPPORT_RGB_COLOR, SUPPORT_EFFECT, Light, PLATFORM_SCHEMA) + ATTR_BRIGHTNESS, ATTR_HS_COLOR, ATTR_EFFECT, SUPPORT_BRIGHTNESS, + SUPPORT_COLOR, SUPPORT_EFFECT, Light, PLATFORM_SCHEMA) from homeassistant.const import (CONF_HOST, CONF_PORT, CONF_NAME) import homeassistant.helpers.config_validation as cv +import homeassistant.util.color as color_util _LOGGER = logging.getLogger(__name__) @@ -40,7 +41,7 @@ DEFAULT_EFFECT_LIST = ['HDMI', 'Cinema brighten lights', 'Cinema dim lights', 'Color traces', 'UDP multicast listener', 'UDP listener', 'X-Mas'] -SUPPORT_HYPERION = (SUPPORT_RGB_COLOR | SUPPORT_BRIGHTNESS | SUPPORT_EFFECT) +SUPPORT_HYPERION = (SUPPORT_COLOR | SUPPORT_BRIGHTNESS | SUPPORT_EFFECT) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_HOST): cv.string, @@ -107,9 +108,9 @@ class Hyperion(Light): return self._brightness @property - def rgb_color(self): - """Return last RGB color value set.""" - return self._rgb_color + def hs_color(self): + """Return last color value set.""" + return color_util.color_RGB_to_hs(*self._rgb_color) @property def is_on(self): @@ -138,8 +139,8 @@ class Hyperion(Light): def turn_on(self, **kwargs): """Turn the lights on.""" - if ATTR_RGB_COLOR in kwargs: - rgb_color = kwargs[ATTR_RGB_COLOR] + if ATTR_HS_COLOR in kwargs: + rgb_color = color_util.color_hs_to_RGB(*kwargs[ATTR_HS_COLOR]) elif self._rgb_mem == [0, 0, 0]: rgb_color = self._default_color else: diff --git a/homeassistant/components/light/iglo.py b/homeassistant/components/light/iglo.py index c7de8d8bede..77e3972968c 100644 --- a/homeassistant/components/light/iglo.py +++ b/homeassistant/components/light/iglo.py @@ -10,8 +10,8 @@ import math import voluptuous as vol from homeassistant.components.light import ( - ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_RGB_COLOR, - SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_RGB_COLOR, SUPPORT_EFFECT, + ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_HS_COLOR, + SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_COLOR, SUPPORT_EFFECT, PLATFORM_SCHEMA, Light) from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT import homeassistant.helpers.config_validation as cv @@ -77,9 +77,9 @@ class IGloLamp(Light): self._lamp.min_kelvin)) @property - def rgb_color(self): - """Return the RGB value.""" - return self._lamp.state()['rgb'] + def hs_color(self): + """Return the hs value.""" + return color_util.color_RGB_to_hsv(*self._lamp.state()['rgb']) @property def effect(self): @@ -95,7 +95,7 @@ class IGloLamp(Light): def supported_features(self): """Flag supported features.""" return (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | - SUPPORT_RGB_COLOR | SUPPORT_EFFECT) + SUPPORT_COLOR | SUPPORT_EFFECT) @property def is_on(self): @@ -111,8 +111,8 @@ class IGloLamp(Light): self._lamp.brightness(brightness) return - if ATTR_RGB_COLOR in kwargs: - rgb = kwargs[ATTR_RGB_COLOR] + if ATTR_HS_COLOR in kwargs: + rgb = color_util.color_hs_to_RGB(*kwargs[ATTR_HS_COLOR]) self._lamp.rgb(*rgb) return diff --git a/homeassistant/components/light/knx.py b/homeassistant/components/light/knx.py index 83083e34bad..18446951735 100644 --- a/homeassistant/components/light/knx.py +++ b/homeassistant/components/light/knx.py @@ -9,11 +9,12 @@ import voluptuous as vol from homeassistant.components.knx import ATTR_DISCOVER_DEVICES, DATA_KNX from homeassistant.components.light import ( - ATTR_BRIGHTNESS, ATTR_RGB_COLOR, PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, - SUPPORT_RGB_COLOR, Light) + ATTR_BRIGHTNESS, ATTR_HS_COLOR, PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, + SUPPORT_COLOR, Light) from homeassistant.const import CONF_NAME from homeassistant.core import callback import homeassistant.helpers.config_validation as cv +import homeassistant.util.color as color_util CONF_ADDRESS = 'address' CONF_STATE_ADDRESS = 'state_address' @@ -114,15 +115,10 @@ class KNXLight(Light): None @property - def xy_color(self): - """Return the XY color value [float, float].""" - return None - - @property - def rgb_color(self): - """Return the RBG color value.""" + def hs_color(self): + """Return the HS color value.""" if self.device.supports_color: - return self.device.current_color + return color_util.color_RGB_to_hs(*self.device.current_color) return None @property @@ -157,7 +153,7 @@ class KNXLight(Light): if self.device.supports_brightness: flags |= SUPPORT_BRIGHTNESS if self.device.supports_color: - flags |= SUPPORT_RGB_COLOR + flags |= SUPPORT_COLOR return flags async def async_turn_on(self, **kwargs): @@ -165,9 +161,10 @@ class KNXLight(Light): if ATTR_BRIGHTNESS in kwargs: if self.device.supports_brightness: await self.device.set_brightness(int(kwargs[ATTR_BRIGHTNESS])) - elif ATTR_RGB_COLOR in kwargs: + elif ATTR_HS_COLOR in kwargs: if self.device.supports_color: - await self.device.set_color(kwargs[ATTR_RGB_COLOR]) + await self.device.set_color(color_util.color_hs_to_RGB( + *kwargs[ATTR_HS_COLOR])) else: await self.device.set_on() diff --git a/homeassistant/components/light/lifx.py b/homeassistant/components/light/lifx.py index 0bb65a78c6e..dff5ccd42ac 100644 --- a/homeassistant/components/light/lifx.py +++ b/homeassistant/components/light/lifx.py @@ -16,10 +16,10 @@ import voluptuous as vol from homeassistant import util from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_BRIGHTNESS_PCT, ATTR_COLOR_NAME, ATTR_COLOR_TEMP, - ATTR_EFFECT, ATTR_KELVIN, ATTR_RGB_COLOR, ATTR_TRANSITION, ATTR_XY_COLOR, - DOMAIN, LIGHT_TURN_ON_SCHEMA, PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, - SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, SUPPORT_RGB_COLOR, SUPPORT_TRANSITION, - SUPPORT_XY_COLOR, VALID_BRIGHTNESS, VALID_BRIGHTNESS_PCT, Light, + ATTR_EFFECT, ATTR_HS_COLOR, ATTR_KELVIN, ATTR_RGB_COLOR, ATTR_TRANSITION, + ATTR_XY_COLOR, COLOR_GROUP, DOMAIN, LIGHT_TURN_ON_SCHEMA, PLATFORM_SCHEMA, + SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, + SUPPORT_TRANSITION, VALID_BRIGHTNESS, VALID_BRIGHTNESS_PCT, Light, preprocess_turn_on_alternatives) from homeassistant.const import ATTR_ENTITY_ID, EVENT_HOMEASSISTANT_STOP from homeassistant.core import callback @@ -87,11 +87,22 @@ LIFX_EFFECT_SCHEMA = vol.Schema({ LIFX_EFFECT_PULSE_SCHEMA = LIFX_EFFECT_SCHEMA.extend({ ATTR_BRIGHTNESS: VALID_BRIGHTNESS, ATTR_BRIGHTNESS_PCT: VALID_BRIGHTNESS_PCT, - ATTR_COLOR_NAME: cv.string, - ATTR_RGB_COLOR: vol.All(vol.ExactSequence((cv.byte, cv.byte, cv.byte)), - vol.Coerce(tuple)), - ATTR_COLOR_TEMP: vol.All(vol.Coerce(int), vol.Range(min=1)), - ATTR_KELVIN: vol.All(vol.Coerce(int), vol.Range(min=0)), + vol.Exclusive(ATTR_COLOR_NAME, COLOR_GROUP): cv.string, + vol.Exclusive(ATTR_RGB_COLOR, COLOR_GROUP): + vol.All(vol.ExactSequence((cv.byte, cv.byte, cv.byte)), + vol.Coerce(tuple)), + vol.Exclusive(ATTR_XY_COLOR, COLOR_GROUP): + vol.All(vol.ExactSequence((cv.small_float, cv.small_float)), + vol.Coerce(tuple)), + vol.Exclusive(ATTR_HS_COLOR, COLOR_GROUP): + vol.All(vol.ExactSequence( + (vol.All(vol.Coerce(float), vol.Range(min=0, max=360)), + vol.All(vol.Coerce(float), vol.Range(min=0, max=100)))), + vol.Coerce(tuple)), + vol.Exclusive(ATTR_COLOR_TEMP, COLOR_GROUP): + vol.All(vol.Coerce(int), vol.Range(min=1)), + vol.Exclusive(ATTR_KELVIN, COLOR_GROUP): + vol.All(vol.Coerce(int), vol.Range(min=0)), ATTR_PERIOD: vol.All(vol.Coerce(float), vol.Range(min=0.05)), ATTR_CYCLES: vol.All(vol.Coerce(float), vol.Range(min=1)), ATTR_MODE: vol.In(PULSE_MODES), @@ -168,16 +179,8 @@ def find_hsbk(**kwargs): preprocess_turn_on_alternatives(kwargs) - if ATTR_RGB_COLOR in kwargs: - hue, saturation, brightness = \ - color_util.color_RGB_to_hsv(*kwargs[ATTR_RGB_COLOR]) - hue = int(hue / 360 * 65535) - saturation = int(saturation / 100 * 65535) - brightness = int(brightness / 100 * 65535) - kelvin = 3500 - - if ATTR_XY_COLOR in kwargs: - hue, saturation = color_util.color_xy_to_hs(*kwargs[ATTR_XY_COLOR]) + if ATTR_HS_COLOR in kwargs: + hue, saturation = kwargs[ATTR_HS_COLOR] hue = int(hue / 360 * 65535) saturation = int(saturation / 100 * 65535) kelvin = 3500 @@ -585,7 +588,7 @@ class LIFXColor(LIFXLight): def supported_features(self): """Flag supported features.""" support = super().supported_features - support |= SUPPORT_RGB_COLOR | SUPPORT_XY_COLOR + support |= SUPPORT_COLOR return support @property @@ -598,15 +601,12 @@ class LIFXColor(LIFXLight): ] @property - def rgb_color(self): - """Return the RGB value.""" - hue, sat, bri, _ = self.device.color - + def hs_color(self): + """Return the hs value.""" + hue, sat, _, _ = self.device.color hue = hue / 65535 * 360 sat = sat / 65535 * 100 - bri = bri / 65535 * 100 - - return color_util.color_hsv_to_RGB(hue, sat, bri) + return (hue, sat) class LIFXStrip(LIFXColor): diff --git a/homeassistant/components/light/lifx_legacy.py b/homeassistant/components/light/lifx_legacy.py index cf3dba848a8..490eeb6ecab 100644 --- a/homeassistant/components/light/lifx_legacy.py +++ b/homeassistant/components/light/lifx_legacy.py @@ -7,14 +7,13 @@ not yet support Windows. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/light.lifx/ """ -import colorsys import logging import voluptuous as vol from homeassistant.components.light import ( - ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_RGB_COLOR, ATTR_TRANSITION, - SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_RGB_COLOR, + ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, ATTR_TRANSITION, + SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_COLOR, SUPPORT_TRANSITION, Light, PLATFORM_SCHEMA) from homeassistant.helpers.event import track_time_change from homeassistant.util.color import ( @@ -37,7 +36,7 @@ TEMP_MAX_HASS = 500 TEMP_MIN = 2500 TEMP_MIN_HASS = 154 -SUPPORT_LIFX = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_RGB_COLOR | +SUPPORT_LIFX = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_COLOR | SUPPORT_TRANSITION) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ @@ -129,17 +128,6 @@ class LIFX(object): self._liffylights.probe(address) -def convert_rgb_to_hsv(rgb): - """Convert Home Assistant RGB values to HSV values.""" - red, green, blue = [_ / BYTE_MAX for _ in rgb] - - hue, saturation, brightness = colorsys.rgb_to_hsv(red, green, blue) - - return [int(hue * SHORT_MAX), - int(saturation * SHORT_MAX), - int(brightness * SHORT_MAX)] - - class LIFXLight(Light): """Representation of a LIFX light.""" @@ -170,11 +158,9 @@ class LIFXLight(Light): return self._ip @property - def rgb_color(self): - """Return the RGB value.""" - _LOGGER.debug( - "rgb_color: [%d %d %d]", self._rgb[0], self._rgb[1], self._rgb[2]) - return self._rgb + def hs_color(self): + """Return the hs value.""" + return (self._hue / 65535 * 360, self._sat / 65535 * 100) @property def brightness(self): @@ -209,13 +195,13 @@ class LIFXLight(Light): else: fade = 0 - if ATTR_RGB_COLOR in kwargs: - hue, saturation, brightness = \ - convert_rgb_to_hsv(kwargs[ATTR_RGB_COLOR]) + if ATTR_HS_COLOR in kwargs: + hue, saturation = kwargs[ATTR_HS_COLOR] + hue = hue / 360 * 65535 + saturation = saturation / 100 * 65535 else: hue = self._hue saturation = self._sat - brightness = self._bri if ATTR_BRIGHTNESS in kwargs: brightness = kwargs[ATTR_BRIGHTNESS] * (BYTE_MAX + 1) @@ -265,16 +251,3 @@ class LIFXLight(Light): self._sat = sat self._bri = bri self._kel = kel - - red, green, blue = colorsys.hsv_to_rgb(hue / SHORT_MAX, - sat / SHORT_MAX, - bri / SHORT_MAX) - - red = int(red * BYTE_MAX) - green = int(green * BYTE_MAX) - blue = int(blue * BYTE_MAX) - - _LOGGER.debug("set_color: %d %d %d %d [%d %d %d]", - hue, sat, bri, kel, red, green, blue) - - self._rgb = [red, green, blue] diff --git a/homeassistant/components/light/limitlessled.py b/homeassistant/components/light/limitlessled.py index f011792a15c..5a6a0a34959 100644 --- a/homeassistant/components/light/limitlessled.py +++ b/homeassistant/components/light/limitlessled.py @@ -12,12 +12,13 @@ import voluptuous as vol from homeassistant.const import ( CONF_NAME, CONF_HOST, CONF_PORT, CONF_TYPE, STATE_ON) from homeassistant.components.light import ( - ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_FLASH, ATTR_RGB_COLOR, + ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_FLASH, ATTR_HS_COLOR, ATTR_TRANSITION, EFFECT_COLORLOOP, EFFECT_WHITE, FLASH_LONG, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, SUPPORT_FLASH, - SUPPORT_RGB_COLOR, SUPPORT_TRANSITION, Light, PLATFORM_SCHEMA) + SUPPORT_COLOR, SUPPORT_TRANSITION, Light, PLATFORM_SCHEMA) import homeassistant.helpers.config_validation as cv -from homeassistant.util.color import color_temperature_mired_to_kelvin +from homeassistant.util.color import ( + color_temperature_mired_to_kelvin, color_hs_to_RGB) from homeassistant.helpers.restore_state import async_get_last_state REQUIREMENTS = ['limitlessled==1.1.0'] @@ -40,19 +41,19 @@ LED_TYPE = ['rgbw', 'rgbww', 'white', 'bridge-led', 'dimmer'] EFFECT_NIGHT = 'night' -RGB_BOUNDARY = 40 +MIN_SATURATION = 10 -WHITE = [255, 255, 255] +WHITE = [0, 0] SUPPORT_LIMITLESSLED_WHITE = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_TRANSITION) SUPPORT_LIMITLESSLED_DIMMER = (SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION) SUPPORT_LIMITLESSLED_RGB = (SUPPORT_BRIGHTNESS | SUPPORT_EFFECT | - SUPPORT_FLASH | SUPPORT_RGB_COLOR | + SUPPORT_FLASH | SUPPORT_COLOR | SUPPORT_TRANSITION) SUPPORT_LIMITLESSLED_RGBWW = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_EFFECT | SUPPORT_FLASH | - SUPPORT_RGB_COLOR | SUPPORT_TRANSITION) + SUPPORT_COLOR | SUPPORT_TRANSITION) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_BRIDGES): vol.All(cv.ensure_list, [ @@ -239,7 +240,7 @@ class LimitlessLEDGroup(Light): return self._temperature @property - def rgb_color(self): + def hs_color(self): """Return the color property.""" return self._color @@ -282,17 +283,17 @@ class LimitlessLEDGroup(Light): self._brightness = kwargs[ATTR_BRIGHTNESS] args['brightness'] = self.limitlessled_brightness() - if ATTR_RGB_COLOR in kwargs and self._supported & SUPPORT_RGB_COLOR: - self._color = kwargs[ATTR_RGB_COLOR] + if ATTR_HS_COLOR in kwargs and self._supported & SUPPORT_COLOR: + self._color = kwargs[ATTR_HS_COLOR] # White is a special case. - if min(self._color) > 256 - RGB_BOUNDARY: + if self._color[1] < MIN_SATURATION: pipeline.white() self._color = WHITE else: args['color'] = self.limitlessled_color() if ATTR_COLOR_TEMP in kwargs: - if self._supported & SUPPORT_RGB_COLOR: + if self._supported & SUPPORT_COLOR: pipeline.white() self._color = WHITE if self._supported & SUPPORT_COLOR_TEMP: @@ -333,6 +334,6 @@ class LimitlessLEDGroup(Light): return self._brightness / 255 def limitlessled_color(self): - """Convert Home Assistant RGB list to Color tuple.""" + """Convert Home Assistant HS list to RGB Color tuple.""" from limitlessled import Color - return Color(*tuple(self._color)) + return Color(color_hs_to_RGB(*tuple(self._color))) diff --git a/homeassistant/components/light/mqtt.py b/homeassistant/components/light/mqtt.py index f97e37127b1..a0534ba4e95 100644 --- a/homeassistant/components/light/mqtt.py +++ b/homeassistant/components/light/mqtt.py @@ -12,10 +12,9 @@ import voluptuous as vol from homeassistant.core import callback import homeassistant.components.mqtt as mqtt from homeassistant.components.light import ( - ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_RGB_COLOR, - ATTR_WHITE_VALUE, ATTR_XY_COLOR, Light, SUPPORT_BRIGHTNESS, - SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, SUPPORT_RGB_COLOR, - SUPPORT_WHITE_VALUE, SUPPORT_XY_COLOR) + ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_HS_COLOR, + ATTR_WHITE_VALUE, Light, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, + SUPPORT_EFFECT, SUPPORT_COLOR, SUPPORT_WHITE_VALUE) from homeassistant.const import ( CONF_BRIGHTNESS, CONF_COLOR_TEMP, CONF_EFFECT, CONF_NAME, CONF_OPTIMISTIC, CONF_PAYLOAD_OFF, CONF_PAYLOAD_ON, @@ -25,6 +24,7 @@ from homeassistant.components.mqtt import ( CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, MqttAvailability) import homeassistant.helpers.config_validation as cv +import homeassistant.util.color as color_util _LOGGER = logging.getLogger(__name__) @@ -191,14 +191,13 @@ class MqttLight(MqttAvailability, Light): self._on_command_type = on_command_type self._state = False self._brightness = None - self._rgb = None + self._hs = None self._color_temp = None self._effect = None self._white_value = None - self._xy = None self._supported_features = 0 self._supported_features |= ( - topic[CONF_RGB_COMMAND_TOPIC] is not None and SUPPORT_RGB_COLOR) + topic[CONF_RGB_COMMAND_TOPIC] is not None and SUPPORT_COLOR) self._supported_features |= ( topic[CONF_BRIGHTNESS_COMMAND_TOPIC] is not None and SUPPORT_BRIGHTNESS) @@ -212,7 +211,7 @@ class MqttLight(MqttAvailability, Light): topic[CONF_WHITE_VALUE_COMMAND_TOPIC] is not None and SUPPORT_WHITE_VALUE) self._supported_features |= ( - topic[CONF_XY_COMMAND_TOPIC] is not None and SUPPORT_XY_COLOR) + topic[CONF_XY_COMMAND_TOPIC] is not None and SUPPORT_COLOR) @asyncio.coroutine def async_added_to_hass(self): @@ -263,19 +262,18 @@ class MqttLight(MqttAvailability, Light): @callback def rgb_received(topic, payload, qos): """Handle new MQTT messages for RGB.""" - self._rgb = [int(val) for val in - templates[CONF_RGB](payload).split(',')] + rgb = [int(val) for val in + templates[CONF_RGB](payload).split(',')] + self._hs = color_util.color_RGB_to_hs(*rgb) self.async_schedule_update_ha_state() if self._topic[CONF_RGB_STATE_TOPIC] is not None: yield from mqtt.async_subscribe( self.hass, self._topic[CONF_RGB_STATE_TOPIC], rgb_received, self._qos) - self._rgb = [255, 255, 255] + self._hs = (0, 0) if self._topic[CONF_RGB_COMMAND_TOPIC] is not None: - self._rgb = [255, 255, 255] - else: - self._rgb = None + self._hs = (0, 0) @callback def color_temp_received(topic, payload, qos): @@ -330,19 +328,18 @@ class MqttLight(MqttAvailability, Light): @callback def xy_received(topic, payload, qos): """Handle new MQTT messages for color.""" - self._xy = [float(val) for val in + xy_color = [float(val) for val in templates[CONF_XY](payload).split(',')] + self._hs = color_util.color_xy_to_hs(*xy_color) self.async_schedule_update_ha_state() if self._topic[CONF_XY_STATE_TOPIC] is not None: yield from mqtt.async_subscribe( self.hass, self._topic[CONF_XY_STATE_TOPIC], xy_received, self._qos) - self._xy = [1, 1] + self._hs = (0, 0) if self._topic[CONF_XY_COMMAND_TOPIC] is not None: - self._xy = [1, 1] - else: - self._xy = None + self._hs = (0, 0) @property def brightness(self): @@ -350,9 +347,9 @@ class MqttLight(MqttAvailability, Light): return self._brightness @property - def rgb_color(self): - """Return the RGB color value.""" - return self._rgb + def hs_color(self): + """Return the hs color value.""" + return self._hs @property def color_temp(self): @@ -364,11 +361,6 @@ class MqttLight(MqttAvailability, Light): """Return the white property.""" return self._white_value - @property - def xy_color(self): - """Return the RGB color value.""" - return self._xy - @property def should_poll(self): """No polling needed for a MQTT light.""" @@ -426,24 +418,43 @@ class MqttLight(MqttAvailability, Light): kwargs[ATTR_BRIGHTNESS] = self._brightness if \ self._brightness else 255 - if ATTR_RGB_COLOR in kwargs and \ + if ATTR_HS_COLOR in kwargs and \ self._topic[CONF_RGB_COMMAND_TOPIC] is not None: + hs_color = kwargs[ATTR_HS_COLOR] + brightness = kwargs.get( + ATTR_BRIGHTNESS, self._brightness if self._brightness else 255) + rgb = color_util.color_hsv_to_RGB( + hs_color[0], hs_color[1], brightness / 255 * 100) tpl = self._templates[CONF_RGB_COMMAND_TEMPLATE] if tpl: - colors = ('red', 'green', 'blue') - variables = {key: val for key, val in - zip(colors, kwargs[ATTR_RGB_COLOR])} - rgb_color_str = tpl.async_render(variables) + rgb_color_str = tpl.async_render({ + 'red': rgb[0], + 'green': rgb[1], + 'blue': rgb[2], + }) else: - rgb_color_str = '{},{},{}'.format(*kwargs[ATTR_RGB_COLOR]) + rgb_color_str = '{},{},{}'.format(*rgb) mqtt.async_publish( self.hass, self._topic[CONF_RGB_COMMAND_TOPIC], rgb_color_str, self._qos, self._retain) if self._optimistic_rgb: - self._rgb = kwargs[ATTR_RGB_COLOR] + self._hs = kwargs[ATTR_HS_COLOR] + should_update = True + + if ATTR_HS_COLOR in kwargs and \ + self._topic[CONF_XY_COMMAND_TOPIC] is not None: + + xy_color = color_util.color_hs_to_xy(*kwargs[ATTR_HS_COLOR]) + mqtt.async_publish( + self.hass, self._topic[CONF_XY_COMMAND_TOPIC], + '{},{}'.format(*xy_color), self._qos, + self._retain) + + if self._optimistic_xy: + self._hs = kwargs[ATTR_HS_COLOR] should_update = True if ATTR_BRIGHTNESS in kwargs and \ @@ -493,18 +504,6 @@ class MqttLight(MqttAvailability, Light): self._white_value = kwargs[ATTR_WHITE_VALUE] should_update = True - if ATTR_XY_COLOR in kwargs and \ - self._topic[CONF_XY_COMMAND_TOPIC] is not None: - - mqtt.async_publish( - self.hass, self._topic[CONF_XY_COMMAND_TOPIC], - '{},{}'.format(*kwargs[ATTR_XY_COLOR]), self._qos, - self._retain) - - if self._optimistic_xy: - self._xy = kwargs[ATTR_XY_COLOR] - should_update = True - if self._on_command_type == 'last': mqtt.async_publish(self.hass, self._topic[CONF_COMMAND_TOPIC], self._payload['on'], self._qos, self._retain) diff --git a/homeassistant/components/light/mqtt_json.py b/homeassistant/components/light/mqtt_json.py index 19747b89ca0..25212e45c60 100644 --- a/homeassistant/components/light/mqtt_json.py +++ b/homeassistant/components/light/mqtt_json.py @@ -13,10 +13,10 @@ from homeassistant.core import callback import homeassistant.components.mqtt as mqtt from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_FLASH, - ATTR_RGB_COLOR, ATTR_TRANSITION, ATTR_WHITE_VALUE, ATTR_XY_COLOR, + ATTR_TRANSITION, ATTR_WHITE_VALUE, ATTR_HS_COLOR, FLASH_LONG, FLASH_SHORT, Light, PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, - SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, SUPPORT_FLASH, SUPPORT_RGB_COLOR, - SUPPORT_TRANSITION, SUPPORT_WHITE_VALUE, SUPPORT_XY_COLOR) + SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, SUPPORT_FLASH, SUPPORT_COLOR, + SUPPORT_TRANSITION, SUPPORT_WHITE_VALUE) from homeassistant.components.light.mqtt import CONF_BRIGHTNESS_SCALE from homeassistant.const import ( CONF_BRIGHTNESS, CONF_COLOR_TEMP, CONF_EFFECT, @@ -26,6 +26,7 @@ from homeassistant.components.mqtt import ( CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, CONF_RETAIN, MqttAvailability) import homeassistant.helpers.config_validation as cv +import homeassistant.util.color as color_util _LOGGER = logging.getLogger(__name__) @@ -143,31 +144,26 @@ class MqttJson(MqttAvailability, Light): else: self._effect = None - if rgb: - self._rgb = [0, 0, 0] + if rgb or xy: + self._hs = [0, 0] else: - self._rgb = None + self._hs = None if white_value: self._white_value = 255 else: self._white_value = None - if xy: - self._xy = [1, 1] - else: - self._xy = None - self._flash_times = flash_times self._brightness_scale = brightness_scale self._supported_features = (SUPPORT_TRANSITION | SUPPORT_FLASH) - self._supported_features |= (rgb and SUPPORT_RGB_COLOR) + self._supported_features |= (rgb and SUPPORT_COLOR) self._supported_features |= (brightness and SUPPORT_BRIGHTNESS) self._supported_features |= (color_temp and SUPPORT_COLOR_TEMP) self._supported_features |= (effect and SUPPORT_EFFECT) self._supported_features |= (white_value and SUPPORT_WHITE_VALUE) - self._supported_features |= (xy and SUPPORT_XY_COLOR) + self._supported_features |= (xy and SUPPORT_COLOR) @asyncio.coroutine def async_added_to_hass(self): @@ -184,17 +180,26 @@ class MqttJson(MqttAvailability, Light): elif values['state'] == 'OFF': self._state = False - if self._rgb is not None: + if self._hs is not None: try: red = int(values['color']['r']) green = int(values['color']['g']) blue = int(values['color']['b']) - self._rgb = [red, green, blue] + self._hs = color_util.color_RGB_to_hs(red, green, blue) except KeyError: pass except ValueError: _LOGGER.warning("Invalid RGB color value received") + try: + x_color = float(values['color']['x']) + y_color = float(values['color']['y']) + + self._hs = color_util.color_xy_to_hs(x_color, y_color) + except KeyError: + pass + except ValueError: + _LOGGER.warning("Invalid XY color value received") if self._brightness is not None: try: @@ -230,17 +235,6 @@ class MqttJson(MqttAvailability, Light): except ValueError: _LOGGER.warning("Invalid white value received") - if self._xy is not None: - try: - x_color = float(values['color']['x']) - y_color = float(values['color']['y']) - - self._xy = [x_color, y_color] - except KeyError: - pass - except ValueError: - _LOGGER.warning("Invalid XY color value received") - self.async_schedule_update_ha_state() if self._topic[CONF_STATE_TOPIC] is not None: @@ -269,20 +263,15 @@ class MqttJson(MqttAvailability, Light): return self._effect_list @property - def rgb_color(self): - """Return the RGB color value.""" - return self._rgb + def hs_color(self): + """Return the hs color value.""" + return self._hs @property def white_value(self): """Return the white property.""" return self._white_value - @property - def xy_color(self): - """Return the XY color value.""" - return self._xy - @property def should_poll(self): """No polling needed for a MQTT light.""" @@ -318,15 +307,23 @@ class MqttJson(MqttAvailability, Light): message = {'state': 'ON'} - if ATTR_RGB_COLOR in kwargs: + if ATTR_HS_COLOR in kwargs: + hs_color = kwargs[ATTR_HS_COLOR] + brightness = kwargs.get( + ATTR_BRIGHTNESS, self._brightness if self._brightness else 255) + rgb = color_util.color_hsv_to_RGB( + hs_color[0], hs_color[1], brightness / 255 * 100) + xy_color = color_util.color_hs_to_xy(*kwargs[ATTR_HS_COLOR]) message['color'] = { - 'r': kwargs[ATTR_RGB_COLOR][0], - 'g': kwargs[ATTR_RGB_COLOR][1], - 'b': kwargs[ATTR_RGB_COLOR][2] + 'r': rgb[0], + 'g': rgb[1], + 'b': rgb[2], + 'x': xy_color[0], + 'y': xy_color[1], } if self._optimistic: - self._rgb = kwargs[ATTR_RGB_COLOR] + self._hs = kwargs[ATTR_HS_COLOR] should_update = True if ATTR_FLASH in kwargs: @@ -370,16 +367,6 @@ class MqttJson(MqttAvailability, Light): self._white_value = kwargs[ATTR_WHITE_VALUE] should_update = True - if ATTR_XY_COLOR in kwargs: - message['color'] = { - 'x': kwargs[ATTR_XY_COLOR][0], - 'y': kwargs[ATTR_XY_COLOR][1] - } - - if self._optimistic: - self._xy = kwargs[ATTR_XY_COLOR] - should_update = True - mqtt.async_publish( self.hass, self._topic[CONF_COMMAND_TOPIC], json.dumps(message), self._qos, self._retain) diff --git a/homeassistant/components/light/mqtt_template.py b/homeassistant/components/light/mqtt_template.py index de0f6d934c6..06a94cd23b4 100644 --- a/homeassistant/components/light/mqtt_template.py +++ b/homeassistant/components/light/mqtt_template.py @@ -12,15 +12,16 @@ from homeassistant.core import callback import homeassistant.components.mqtt as mqtt from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_FLASH, - ATTR_RGB_COLOR, ATTR_TRANSITION, ATTR_WHITE_VALUE, Light, PLATFORM_SCHEMA, + ATTR_HS_COLOR, ATTR_TRANSITION, ATTR_WHITE_VALUE, Light, PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, SUPPORT_FLASH, - SUPPORT_RGB_COLOR, SUPPORT_TRANSITION, SUPPORT_WHITE_VALUE) + SUPPORT_COLOR, SUPPORT_TRANSITION, SUPPORT_WHITE_VALUE) from homeassistant.const import CONF_NAME, CONF_OPTIMISTIC, STATE_ON, STATE_OFF from homeassistant.components.mqtt import ( CONF_AVAILABILITY_TOPIC, CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, CONF_RETAIN, MqttAvailability) import homeassistant.helpers.config_validation as cv +import homeassistant.util.color as color_util _LOGGER = logging.getLogger(__name__) @@ -142,9 +143,9 @@ class MqttTemplate(MqttAvailability, Light): if (self._templates[CONF_RED_TEMPLATE] is not None and self._templates[CONF_GREEN_TEMPLATE] is not None and self._templates[CONF_BLUE_TEMPLATE] is not None): - self._rgb = [0, 0, 0] + self._hs = [0, 0] else: - self._rgb = None + self._hs = None self._effect = None for tpl in self._templates.values(): @@ -186,17 +187,18 @@ class MqttTemplate(MqttAvailability, Light): except ValueError: _LOGGER.warning("Invalid color temperature value received") - if self._rgb is not None: + if self._hs is not None: try: - self._rgb[0] = int( + red = int( self._templates[CONF_RED_TEMPLATE]. async_render_with_possible_json_value(payload)) - self._rgb[1] = int( + green = int( self._templates[CONF_GREEN_TEMPLATE]. async_render_with_possible_json_value(payload)) - self._rgb[2] = int( + blue = int( self._templates[CONF_BLUE_TEMPLATE]. async_render_with_possible_json_value(payload)) + self._hs = color_util.color_RGB_to_hs(red, green, blue) except ValueError: _LOGGER.warning("Invalid color value received") @@ -236,9 +238,9 @@ class MqttTemplate(MqttAvailability, Light): return self._color_temp @property - def rgb_color(self): - """Return the RGB color value [int, int, int].""" - return self._rgb + def hs_color(self): + """Return the hs color value [int, int].""" + return self._hs @property def white_value(self): @@ -300,13 +302,18 @@ class MqttTemplate(MqttAvailability, Light): if self._optimistic: self._color_temp = kwargs[ATTR_COLOR_TEMP] - if ATTR_RGB_COLOR in kwargs: - values['red'] = kwargs[ATTR_RGB_COLOR][0] - values['green'] = kwargs[ATTR_RGB_COLOR][1] - values['blue'] = kwargs[ATTR_RGB_COLOR][2] + if ATTR_HS_COLOR in kwargs: + hs_color = kwargs[ATTR_HS_COLOR] + brightness = kwargs.get( + ATTR_BRIGHTNESS, self._brightness if self._brightness else 255) + rgb = color_util.color_hsv_to_RGB( + hs_color[0], hs_color[1], brightness / 255 * 100) + values['red'] = rgb[0] + values['green'] = rgb[1] + values['blue'] = rgb[2] if self._optimistic: - self._rgb = kwargs[ATTR_RGB_COLOR] + self._hs = kwargs[ATTR_HS_COLOR] if ATTR_WHITE_VALUE in kwargs: values['white_value'] = int(kwargs[ATTR_WHITE_VALUE]) @@ -360,8 +367,8 @@ class MqttTemplate(MqttAvailability, Light): features = (SUPPORT_FLASH | SUPPORT_TRANSITION) if self._brightness is not None: features = features | SUPPORT_BRIGHTNESS - if self._rgb is not None: - features = features | SUPPORT_RGB_COLOR + if self._hs is not None: + features = features | SUPPORT_COLOR if self._effect_list is not None: features = features | SUPPORT_EFFECT if self._color_temp is not None: diff --git a/homeassistant/components/light/mysensors.py b/homeassistant/components/light/mysensors.py index a37553017e7..26e20ff387d 100644 --- a/homeassistant/components/light/mysensors.py +++ b/homeassistant/components/light/mysensors.py @@ -6,12 +6,13 @@ https://home-assistant.io/components/light.mysensors/ """ from homeassistant.components import mysensors from homeassistant.components.light import ( - ATTR_BRIGHTNESS, ATTR_RGB_COLOR, ATTR_WHITE_VALUE, DOMAIN, - SUPPORT_BRIGHTNESS, SUPPORT_RGB_COLOR, SUPPORT_WHITE_VALUE, Light) + ATTR_BRIGHTNESS, ATTR_HS_COLOR, ATTR_WHITE_VALUE, DOMAIN, + SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_WHITE_VALUE, Light) from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.util.color import rgb_hex_to_rgb_list +import homeassistant.util.color as color_util -SUPPORT_MYSENSORS = (SUPPORT_BRIGHTNESS | SUPPORT_RGB_COLOR | +SUPPORT_MYSENSORS = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR | SUPPORT_WHITE_VALUE) @@ -35,7 +36,7 @@ class MySensorsLight(mysensors.MySensorsEntity, Light): super().__init__(*args) self._state = None self._brightness = None - self._rgb = None + self._hs = None self._white = None @property @@ -44,9 +45,9 @@ class MySensorsLight(mysensors.MySensorsEntity, Light): return self._brightness @property - def rgb_color(self): - """Return the RGB color value [int, int, int].""" - return self._rgb + def hs_color(self): + """Return the hs color value [int, int].""" + return self._hs @property def white_value(self): @@ -103,10 +104,10 @@ class MySensorsLight(mysensors.MySensorsEntity, Light): def _turn_on_rgb_and_w(self, hex_template, **kwargs): """Turn on RGB or RGBW child device.""" - rgb = self._rgb + rgb = color_util.color_hs_to_RGB(*self._hs) white = self._white hex_color = self._values.get(self.value_type) - new_rgb = kwargs.get(ATTR_RGB_COLOR) + new_rgb = color_util.color_hs_to_RGB(*kwargs.get(ATTR_HS_COLOR)) new_white = kwargs.get(ATTR_WHITE_VALUE) if new_rgb is None and new_white is None: @@ -126,7 +127,7 @@ class MySensorsLight(mysensors.MySensorsEntity, Light): if self.gateway.optimistic: # optimistically assume that light has changed state - self._rgb = rgb + self._hs = color_util.color_RGB_to_hs(*rgb) self._white = white self._values[self.value_type] = hex_color @@ -160,7 +161,7 @@ class MySensorsLight(mysensors.MySensorsEntity, Light): color_list = rgb_hex_to_rgb_list(value) if len(color_list) > 3: self._white = color_list.pop() - self._rgb = color_list + self._hs = color_util.color_RGB_to_hs(*color_list) class MySensorsLightDimmer(MySensorsLight): diff --git a/homeassistant/components/light/mystrom.py b/homeassistant/components/light/mystrom.py index 9f049dd2e8a..d9312e6aadc 100644 --- a/homeassistant/components/light/mystrom.py +++ b/homeassistant/components/light/mystrom.py @@ -11,10 +11,9 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.components.light import ( Light, PLATFORM_SCHEMA, ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, - SUPPORT_EFFECT, ATTR_EFFECT, SUPPORT_FLASH, SUPPORT_RGB_COLOR, - ATTR_RGB_COLOR) + SUPPORT_EFFECT, ATTR_EFFECT, SUPPORT_FLASH, SUPPORT_COLOR, + ATTR_HS_COLOR) from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, STATE_UNKNOWN -from homeassistant.util.color import color_RGB_to_hsv, color_hsv_to_RGB REQUIREMENTS = ['python-mystrom==0.3.8'] @@ -24,7 +23,7 @@ DEFAULT_NAME = 'myStrom bulb' SUPPORT_MYSTROM = ( SUPPORT_BRIGHTNESS | SUPPORT_EFFECT | SUPPORT_FLASH | - SUPPORT_RGB_COLOR + SUPPORT_COLOR ) EFFECT_RAINBOW = 'rainbow' @@ -91,9 +90,9 @@ class MyStromLight(Light): return self._brightness @property - def rgb_color(self): + def hs_color(self): """Return the color of the light.""" - return color_hsv_to_RGB(self._color_h, self._color_s, self._brightness) + return self._color_h, self._color_s @property def available(self) -> bool: @@ -117,12 +116,8 @@ class MyStromLight(Light): brightness = kwargs.get(ATTR_BRIGHTNESS, 255) effect = kwargs.get(ATTR_EFFECT) - if ATTR_RGB_COLOR in kwargs: - # New color, compute from RGB - color_h, color_s, brightness = color_RGB_to_hsv( - *kwargs[ATTR_RGB_COLOR] - ) - brightness = brightness / 100 * 255 + if ATTR_HS_COLOR in kwargs: + color_h, color_s = kwargs[ATTR_HS_COLOR] elif ATTR_BRIGHTNESS in kwargs: # Brightness update, keep color color_h, color_s = self._color_h, self._color_s diff --git a/homeassistant/components/light/osramlightify.py b/homeassistant/components/light/osramlightify.py index ff526c4783d..2c44620caca 100644 --- a/homeassistant/components/light/osramlightify.py +++ b/homeassistant/components/light/osramlightify.py @@ -13,15 +13,15 @@ import voluptuous as vol from homeassistant import util from homeassistant.components.light import ( - ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_RGB_COLOR, - ATTR_TRANSITION, ATTR_XY_COLOR, EFFECT_RANDOM, PLATFORM_SCHEMA, - SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, SUPPORT_RGB_COLOR, - SUPPORT_TRANSITION, SUPPORT_XY_COLOR, Light) + ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_HS_COLOR, + ATTR_TRANSITION, EFFECT_RANDOM, PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, + SUPPORT_COLOR, SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, SUPPORT_TRANSITION, + Light) from homeassistant.const import CONF_HOST import homeassistant.helpers.config_validation as cv from homeassistant.util.color import ( - color_temperature_kelvin_to_mired, color_temperature_mired_to_kelvin, - color_xy_brightness_to_RGB) + color_temperature_kelvin_to_mired, color_temperature_mired_to_kelvin) +import homeassistant.util.color as color_util REQUIREMENTS = ['lightify==1.0.6.1'] @@ -35,8 +35,8 @@ MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(milliseconds=100) MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) SUPPORT_OSRAMLIGHTIFY = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | - SUPPORT_EFFECT | SUPPORT_RGB_COLOR | - SUPPORT_TRANSITION | SUPPORT_XY_COLOR) + SUPPORT_EFFECT | SUPPORT_COLOR | + SUPPORT_TRANSITION) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_HOST): cv.string, @@ -113,7 +113,7 @@ class Luminary(Light): self.update_lights = update_lights self._luminary = luminary self._brightness = None - self._rgb = [None] + self._hs = None self._name = None self._temperature = None self._state = False @@ -125,9 +125,9 @@ class Luminary(Light): return self._name @property - def rgb_color(self): - """Last RGB color value set.""" - return self._rgb + def hs_color(self): + """Last hs color value set.""" + return self._hs @property def color_temp(self): @@ -158,42 +158,24 @@ class Luminary(Light): """Turn the device on.""" if ATTR_TRANSITION in kwargs: transition = int(kwargs[ATTR_TRANSITION] * 10) - _LOGGER.debug("turn_on requested transition time for light: " - "%s is: %s", self._name, transition) else: transition = 0 - _LOGGER.debug("turn_on requested transition time for light: " - "%s is: %s", self._name, transition) if ATTR_BRIGHTNESS in kwargs: self._brightness = kwargs[ATTR_BRIGHTNESS] - _LOGGER.debug("turn_on requested brightness for light: %s is: %s ", - self._name, self._brightness) self._luminary.set_luminance( int(self._brightness / 2.55), transition) else: self._luminary.set_onoff(1) - if ATTR_RGB_COLOR in kwargs: - red, green, blue = kwargs[ATTR_RGB_COLOR] - _LOGGER.debug("turn_on requested ATTR_RGB_COLOR for light:" - " %s is: %s %s %s ", - self._name, red, green, blue) - self._luminary.set_rgb(red, green, blue, transition) - - if ATTR_XY_COLOR in kwargs: - x_mired, y_mired = kwargs[ATTR_XY_COLOR] - _LOGGER.debug("turn_on requested ATTR_XY_COLOR for light:" - " %s is: %s,%s", self._name, x_mired, y_mired) - red, green, blue = color_xy_brightness_to_RGB( - x_mired, y_mired, self._brightness) + if ATTR_HS_COLOR in kwargs: + red, green, blue = \ + color_util.color_hs_to_RGB(*kwargs[ATTR_HS_COLOR]) self._luminary.set_rgb(red, green, blue, transition) if ATTR_COLOR_TEMP in kwargs: color_t = kwargs[ATTR_COLOR_TEMP] kelvin = int(color_temperature_mired_to_kelvin(color_t)) - _LOGGER.debug("turn_on requested set_temperature for light: " - "%s: %s", self._name, kelvin) self._luminary.set_temperature(kelvin, transition) if ATTR_EFFECT in kwargs: @@ -202,23 +184,16 @@ class Luminary(Light): self._luminary.set_rgb( random.randrange(0, 255), random.randrange(0, 255), random.randrange(0, 255), transition) - _LOGGER.debug("turn_on requested random effect for light: " - "%s with transition %s", self._name, transition) self.schedule_update_ha_state() def turn_off(self, **kwargs): """Turn the device off.""" - _LOGGER.debug("Attempting to turn off light: %s", self._name) if ATTR_TRANSITION in kwargs: transition = int(kwargs[ATTR_TRANSITION] * 10) - _LOGGER.debug("turn_off requested transition time for light:" - " %s is: %s ", self._name, transition) self._luminary.set_luminance(0, transition) else: transition = 0 - _LOGGER.debug("turn_off requested transition time for light:" - " %s is: %s ", self._name, transition) self._luminary.set_onoff(0) self.schedule_update_ha_state() @@ -240,7 +215,8 @@ class OsramLightifyLight(Luminary): """Update status of a light.""" super().update() self._state = self._luminary.on() - self._rgb = self._luminary.rgb() + rgb = self._luminary.rgb() + self._hs = color_util.color_RGB_to_hs(*rgb) o_temp = self._luminary.temp() if o_temp == 0: self._temperature = None @@ -270,7 +246,8 @@ class OsramLightifyGroup(Luminary): self._light_ids = self._luminary.lights() light = self._bridge.lights()[self._light_ids[0]] self._brightness = int(light.lum() * 2.55) - self._rgb = light.rgb() + rgb = light.rgb() + self._hs = color_util.color_RGB_to_hs(*rgb) o_temp = light.temp() if o_temp == 0: self._temperature = None diff --git a/homeassistant/components/light/piglow.py b/homeassistant/components/light/piglow.py index 40798810c0e..755cf9dca66 100644 --- a/homeassistant/components/light/piglow.py +++ b/homeassistant/components/light/piglow.py @@ -11,15 +11,16 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.components.light import ( - ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, ATTR_RGB_COLOR, SUPPORT_RGB_COLOR, + ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, ATTR_HS_COLOR, SUPPORT_COLOR, Light, PLATFORM_SCHEMA) from homeassistant.const import CONF_NAME +import homeassistant.util.color as color_util REQUIREMENTS = ['piglow==1.2.4'] _LOGGER = logging.getLogger(__name__) -SUPPORT_PIGLOW = (SUPPORT_BRIGHTNESS | SUPPORT_RGB_COLOR) +SUPPORT_PIGLOW = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR) DEFAULT_NAME = 'Piglow' @@ -50,7 +51,7 @@ class PiglowLight(Light): self._name = name self._is_on = False self._brightness = 255 - self._rgb_color = [255, 255, 255] + self._hs_color = [0, 0] @property def name(self): @@ -63,9 +64,9 @@ class PiglowLight(Light): return self._brightness @property - def rgb_color(self): + def hs_color(self): """Read back the color of the light.""" - return self._rgb_color + return self._hs_color @property def supported_features(self): @@ -93,15 +94,15 @@ class PiglowLight(Light): if ATTR_BRIGHTNESS in kwargs: self._brightness = kwargs[ATTR_BRIGHTNESS] - percent_bright = (self._brightness / 255) - if ATTR_RGB_COLOR in kwargs: - self._rgb_color = kwargs[ATTR_RGB_COLOR] - self._piglow.red(int(self._rgb_color[0] * percent_bright)) - self._piglow.green(int(self._rgb_color[1] * percent_bright)) - self._piglow.blue(int(self._rgb_color[2] * percent_bright)) - else: - self._piglow.all(self._brightness) + if ATTR_HS_COLOR in kwargs: + self._hs_color = kwargs[ATTR_HS_COLOR] + + rgb = color_util.color_hsv_to_RGB( + self._hs_color[0], self._hs_color[1], self._brightness / 255 * 100) + self._piglow.red(rgb[0]) + self._piglow.green(rgb[1]) + self._piglow.blue(rgb[2]) self._piglow.show() self._is_on = True self.schedule_update_ha_state() diff --git a/homeassistant/components/light/rpi_gpio_pwm.py b/homeassistant/components/light/rpi_gpio_pwm.py index 55b64bf8a74..9385c4bfb80 100644 --- a/homeassistant/components/light/rpi_gpio_pwm.py +++ b/homeassistant/components/light/rpi_gpio_pwm.py @@ -10,9 +10,10 @@ import voluptuous as vol from homeassistant.const import CONF_NAME, CONF_TYPE from homeassistant.components.light import ( - Light, ATTR_BRIGHTNESS, ATTR_RGB_COLOR, ATTR_TRANSITION, - SUPPORT_BRIGHTNESS, SUPPORT_RGB_COLOR, SUPPORT_TRANSITION, PLATFORM_SCHEMA) + Light, ATTR_BRIGHTNESS, ATTR_HS_COLOR, ATTR_TRANSITION, + SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_TRANSITION, PLATFORM_SCHEMA) import homeassistant.helpers.config_validation as cv +import homeassistant.util.color as color_util REQUIREMENTS = ['pwmled==1.2.1'] @@ -33,10 +34,10 @@ CONF_LED_TYPE_RGB = 'rgb' CONF_LED_TYPE_RGBW = 'rgbw' CONF_LED_TYPES = [CONF_LED_TYPE_SIMPLE, CONF_LED_TYPE_RGB, CONF_LED_TYPE_RGBW] -DEFAULT_COLOR = [255, 255, 255] +DEFAULT_COLOR = [0, 0] SUPPORT_SIMPLE_LED = (SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION) -SUPPORT_RGB_LED = (SUPPORT_BRIGHTNESS | SUPPORT_RGB_COLOR | SUPPORT_TRANSITION) +SUPPORT_RGB_LED = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR | SUPPORT_TRANSITION) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_LEDS): vol.All(cv.ensure_list, [ @@ -169,7 +170,7 @@ class PwmRgbLed(PwmSimpleLed): self._color = DEFAULT_COLOR @property - def rgb_color(self): + def hs_color(self): """Return the color property.""" return self._color @@ -180,8 +181,8 @@ class PwmRgbLed(PwmSimpleLed): def turn_on(self, **kwargs): """Turn on a LED.""" - if ATTR_RGB_COLOR in kwargs: - self._color = kwargs[ATTR_RGB_COLOR] + if ATTR_HS_COLOR in kwargs: + self._color = kwargs[ATTR_HS_COLOR] if ATTR_BRIGHTNESS in kwargs: self._brightness = kwargs[ATTR_BRIGHTNESS] @@ -209,4 +210,5 @@ def _from_hass_brightness(brightness): def _from_hass_color(color): """Convert Home Assistant RGB list to Color tuple.""" from pwmled import Color - return Color(*tuple(color)) + rgb = color_util.color_hs_to_RGB(*color) + return Color(*tuple(rgb)) diff --git a/homeassistant/components/light/sensehat.py b/homeassistant/components/light/sensehat.py index 6c5467f8c6d..6ab2592cedf 100644 --- a/homeassistant/components/light/sensehat.py +++ b/homeassistant/components/light/sensehat.py @@ -10,15 +10,16 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.components.light import ( - ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, ATTR_RGB_COLOR, SUPPORT_RGB_COLOR, + ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, ATTR_HS_COLOR, SUPPORT_COLOR, Light, PLATFORM_SCHEMA) from homeassistant.const import CONF_NAME +import homeassistant.util.color as color_util REQUIREMENTS = ['sense-hat==2.2.0'] _LOGGER = logging.getLogger(__name__) -SUPPORT_SENSEHAT = (SUPPORT_BRIGHTNESS | SUPPORT_RGB_COLOR) +SUPPORT_SENSEHAT = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR) DEFAULT_NAME = 'sensehat' @@ -49,7 +50,7 @@ class SenseHatLight(Light): self._name = name self._is_on = False self._brightness = 255 - self._rgb_color = [255, 255, 255] + self._hs_color = [0, 0] @property def name(self): @@ -62,12 +63,9 @@ class SenseHatLight(Light): return self._brightness @property - def rgb_color(self): - """Read back the color of the light. - - Returns [r, g, b] list with values in range of 0-255. - """ - return self._rgb_color + def hs_color(self): + """Read back the color of the light.""" + return self._hs_color @property def supported_features(self): @@ -93,14 +91,13 @@ class SenseHatLight(Light): """Instruct the light to turn on and set correct brightness & color.""" if ATTR_BRIGHTNESS in kwargs: self._brightness = kwargs[ATTR_BRIGHTNESS] - percent_bright = (self._brightness / 255) - if ATTR_RGB_COLOR in kwargs: - self._rgb_color = kwargs[ATTR_RGB_COLOR] + if ATTR_HS_COLOR in kwargs: + self._hs_color = kwargs[ATTR_HS_COLOR] - self._sensehat.clear(int(self._rgb_color[0] * percent_bright), - int(self._rgb_color[1] * percent_bright), - int(self._rgb_color[2] * percent_bright)) + rgb = color_util.color_hsv_to_RGB( + self._hs_color[0], self._hs_color[1], self._brightness / 255 * 100) + self._sensehat.clear(*rgb) self._is_on = True self.schedule_update_ha_state() diff --git a/homeassistant/components/light/skybell.py b/homeassistant/components/light/skybell.py index 012190023fa..d32183f1468 100644 --- a/homeassistant/components/light/skybell.py +++ b/homeassistant/components/light/skybell.py @@ -8,10 +8,11 @@ import logging from homeassistant.components.light import ( - ATTR_BRIGHTNESS, ATTR_RGB_COLOR, - SUPPORT_BRIGHTNESS, SUPPORT_RGB_COLOR, Light) + ATTR_BRIGHTNESS, ATTR_HS_COLOR, + SUPPORT_BRIGHTNESS, SUPPORT_COLOR, Light) from homeassistant.components.skybell import ( DOMAIN as SKYBELL_DOMAIN, SkybellDevice) +import homeassistant.util.color as color_util DEPENDENCIES = ['skybell'] @@ -54,8 +55,9 @@ class SkybellLight(SkybellDevice, Light): def turn_on(self, **kwargs): """Turn on the light.""" - if ATTR_RGB_COLOR in kwargs: - self._device.led_rgb = kwargs[ATTR_RGB_COLOR] + if ATTR_HS_COLOR in kwargs: + rgb = color_util.color_hs_to_RGB(*kwargs[ATTR_HS_COLOR]) + self._device.led_rgb = rgb elif ATTR_BRIGHTNESS in kwargs: self._device.led_intensity = _to_skybell_level( kwargs[ATTR_BRIGHTNESS]) @@ -77,11 +79,11 @@ class SkybellLight(SkybellDevice, Light): return _to_hass_level(self._device.led_intensity) @property - def rgb_color(self): + def hs_color(self): """Return the color of the light.""" - return self._device.led_rgb + return color_util.color_RGB_to_hs(*self._device.led_rgb) @property def supported_features(self): """Flag supported features.""" - return SUPPORT_BRIGHTNESS | SUPPORT_RGB_COLOR + return SUPPORT_BRIGHTNESS | SUPPORT_COLOR diff --git a/homeassistant/components/light/tikteck.py b/homeassistant/components/light/tikteck.py index c39748e4430..2079638f7f1 100644 --- a/homeassistant/components/light/tikteck.py +++ b/homeassistant/components/light/tikteck.py @@ -10,15 +10,16 @@ import voluptuous as vol from homeassistant.const import CONF_DEVICES, CONF_NAME, CONF_PASSWORD from homeassistant.components.light import ( - ATTR_BRIGHTNESS, ATTR_RGB_COLOR, SUPPORT_BRIGHTNESS, SUPPORT_RGB_COLOR, + ATTR_BRIGHTNESS, ATTR_HS_COLOR, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, Light, PLATFORM_SCHEMA) import homeassistant.helpers.config_validation as cv +import homeassistant.util.color as color_util REQUIREMENTS = ['tikteck==0.4'] _LOGGER = logging.getLogger(__name__) -SUPPORT_TIKTECK_LED = (SUPPORT_BRIGHTNESS | SUPPORT_RGB_COLOR) +SUPPORT_TIKTECK_LED = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR) DEVICE_SCHEMA = vol.Schema({ vol.Optional(CONF_NAME): cv.string, @@ -57,7 +58,7 @@ class TikteckLight(Light): self._address = device['address'] self._password = device['password'] self._brightness = 255 - self._rgb = [255, 255, 255] + self._hs = [0, 0] self._state = False self.is_valid = True self._bulb = tikteck.tikteck( @@ -88,9 +89,9 @@ class TikteckLight(Light): return self._brightness @property - def rgb_color(self): + def hs_color(self): """Return the color property.""" - return self._rgb + return self._hs @property def supported_features(self): @@ -115,16 +116,17 @@ class TikteckLight(Light): """Turn the specified light on.""" self._state = True - rgb = kwargs.get(ATTR_RGB_COLOR) + hs_color = kwargs.get(ATTR_HS_COLOR) brightness = kwargs.get(ATTR_BRIGHTNESS) - if rgb is not None: - self._rgb = rgb + if hs_color is not None: + self._hs = hs_color if brightness is not None: self._brightness = brightness - self.set_state(self._rgb[0], self._rgb[1], self._rgb[2], - self.brightness) + rgb = color_util.color_hs_to_RGB(*self._hs) + + self.set_state(rgb[0], rgb[1], rgb[2], self.brightness) self.schedule_update_ha_state() def turn_off(self, **kwargs): diff --git a/homeassistant/components/light/tplink.py b/homeassistant/components/light/tplink.py index f87d624b83a..0bbec010282 100644 --- a/homeassistant/components/light/tplink.py +++ b/homeassistant/components/light/tplink.py @@ -5,23 +5,20 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/light.tplink/ """ import logging -import colorsys import time import voluptuous as vol from homeassistant.const import (CONF_HOST, CONF_NAME) from homeassistant.components.light import ( - Light, ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_KELVIN, ATTR_RGB_COLOR, - SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_RGB_COLOR, PLATFORM_SCHEMA) + Light, ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_KELVIN, ATTR_HS_COLOR, + SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_COLOR, PLATFORM_SCHEMA) import homeassistant.helpers.config_validation as cv from homeassistant.util.color import \ color_temperature_mired_to_kelvin as mired_to_kelvin from homeassistant.util.color import ( color_temperature_kelvin_to_mired as kelvin_to_mired) -from typing import Tuple - REQUIREMENTS = ['pyHS100==0.3.0'] _LOGGER = logging.getLogger(__name__) @@ -56,22 +53,6 @@ def brightness_from_percentage(percent): return (percent*255.0)/100.0 -# Travis-CI runs too old astroid https://github.com/PyCQA/pylint/issues/1212 -# pylint: disable=invalid-sequence-index -def rgb_to_hsv(rgb: Tuple[float, float, float]) -> Tuple[int, int, int]: - """Convert RGB tuple (values 0-255) to HSV (degrees, %, %).""" - hue, sat, value = colorsys.rgb_to_hsv(rgb[0]/255, rgb[1]/255, rgb[2]/255) - return int(hue * 360), int(sat * 100), int(value * 100) - - -# Travis-CI runs too old astroid https://github.com/PyCQA/pylint/issues/1212 -# pylint: disable=invalid-sequence-index -def hsv_to_rgb(hsv: Tuple[float, float, float]) -> Tuple[int, int, int]: - """Convert HSV tuple (degrees, %, %) to RGB (values 0-255).""" - red, green, blue = colorsys.hsv_to_rgb(hsv[0]/360, hsv[1]/100, hsv[2]/100) - return int(red * 255), int(green * 255), int(blue * 255) - - class TPLinkSmartBulb(Light): """Representation of a TPLink Smart Bulb.""" @@ -83,7 +64,7 @@ class TPLinkSmartBulb(Light): self._available = True self._color_temp = None self._brightness = None - self._rgb = None + self._hs = None self._supported_features = 0 self._emeter_params = {} @@ -114,9 +95,10 @@ class TPLinkSmartBulb(Light): if ATTR_BRIGHTNESS in kwargs: brightness = kwargs.get(ATTR_BRIGHTNESS, self.brightness or 255) self.smartbulb.brightness = brightness_to_percentage(brightness) - if ATTR_RGB_COLOR in kwargs: - rgb = kwargs.get(ATTR_RGB_COLOR) - self.smartbulb.hsv = rgb_to_hsv(rgb) + if ATTR_HS_COLOR in kwargs: + hue, sat = kwargs.get(ATTR_HS_COLOR) + hsv = (hue, sat, 100) + self.smartbulb.hsv = hsv def turn_off(self, **kwargs): """Turn the light off.""" @@ -133,9 +115,9 @@ class TPLinkSmartBulb(Light): return self._brightness @property - def rgb_color(self): - """Return the color in RGB.""" - return self._rgb + def hs_color(self): + """Return the color.""" + return self._hs @property def is_on(self): @@ -168,8 +150,9 @@ class TPLinkSmartBulb(Light): self._color_temp = kelvin_to_mired( self.smartbulb.color_temp) - if self._supported_features & SUPPORT_RGB_COLOR: - self._rgb = hsv_to_rgb(self.smartbulb.hsv) + if self._supported_features & SUPPORT_COLOR: + hue, sat, _ = self.smartbulb.hsv + self._hs = (hue, sat) if self.smartbulb.has_emeter: self._emeter_params[ATTR_CURRENT_POWER_W] = '{:.1f}'.format( @@ -203,4 +186,4 @@ class TPLinkSmartBulb(Light): if self.smartbulb.is_variable_color_temp: self._supported_features += SUPPORT_COLOR_TEMP if self.smartbulb.is_color: - self._supported_features += SUPPORT_RGB_COLOR + self._supported_features += SUPPORT_COLOR diff --git a/homeassistant/components/light/tradfri.py b/homeassistant/components/light/tradfri.py index bb2fa44c15c..1851579a172 100644 --- a/homeassistant/components/light/tradfri.py +++ b/homeassistant/components/light/tradfri.py @@ -10,9 +10,9 @@ import logging from homeassistant.core import callback from homeassistant.const import ATTR_BATTERY_LEVEL from homeassistant.components.light import ( - ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_RGB_COLOR, ATTR_TRANSITION, + ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, ATTR_TRANSITION, SUPPORT_BRIGHTNESS, SUPPORT_TRANSITION, SUPPORT_COLOR_TEMP, - SUPPORT_RGB_COLOR, Light) + SUPPORT_COLOR, Light) from homeassistant.components.light import \ PLATFORM_SCHEMA as LIGHT_PLATFORM_SCHEMA from homeassistant.components.tradfri import KEY_GATEWAY, KEY_TRADFRI_GROUPS, \ @@ -157,7 +157,7 @@ class TradfriLight(Light): self._light_control = None self._light_data = None self._name = None - self._rgb_color = None + self._hs_color = None self._features = SUPPORTED_FEATURES self._temp_supported = False self._available = True @@ -237,9 +237,9 @@ class TradfriLight(Light): ) @property - def rgb_color(self): - """RGB color of the light.""" - return self._rgb_color + def hs_color(self): + """HS color of the light.""" + return self._hs_color @asyncio.coroutine def async_turn_off(self, **kwargs): @@ -252,12 +252,12 @@ class TradfriLight(Light): Instruct the light to turn on. After adding "self._light_data.hexcolor is not None" - for ATTR_RGB_COLOR, this also supports Philips Hue bulbs. + for ATTR_HS_COLOR, this also supports Philips Hue bulbs. """ - if ATTR_RGB_COLOR in kwargs and self._light_data.hex_color is not None: + if ATTR_HS_COLOR in kwargs and self._light_data.hex_color is not None: + rgb = color_util.color_hs_to_RGB(*kwargs[ATTR_HS_COLOR]) yield from self._api( - self._light.light_control.set_rgb_color( - *kwargs[ATTR_RGB_COLOR])) + self._light.light_control.set_rgb_color(*rgb)) elif ATTR_COLOR_TEMP in kwargs and \ self._light_data.hex_color is not None and \ @@ -309,17 +309,17 @@ class TradfriLight(Light): self._light_control = light.light_control self._light_data = light.light_control.lights[0] self._name = light.name - self._rgb_color = None + self._hs_color = None self._features = SUPPORTED_FEATURES if self._light.device_info.manufacturer == IKEA: if self._light_control.can_set_kelvin: self._features |= SUPPORT_COLOR_TEMP if self._light_control.can_set_color: - self._features |= SUPPORT_RGB_COLOR + self._features |= SUPPORT_COLOR else: if self._light_data.hex_color is not None: - self._features |= SUPPORT_RGB_COLOR + self._features |= SUPPORT_COLOR self._temp_supported = self._light.device_info.manufacturer \ in ALLOWED_TEMPERATURES @@ -328,7 +328,8 @@ class TradfriLight(Light): def _observe_update(self, tradfri_device): """Receive new state data for this light.""" self._refresh(tradfri_device) - self._rgb_color = color_util.rgb_hex_to_rgb_list( + rgb = color_util.rgb_hex_to_rgb_list( self._light_data.hex_color_inferred ) + self._hs_color = color_util.color_RGB_to_hs(*rgb) self.async_schedule_update_ha_state() diff --git a/homeassistant/components/light/vera.py b/homeassistant/components/light/vera.py index 102ca814882..6b12e69341d 100644 --- a/homeassistant/components/light/vera.py +++ b/homeassistant/components/light/vera.py @@ -7,10 +7,11 @@ https://home-assistant.io/components/light.vera/ import logging from homeassistant.components.light import ( - ATTR_BRIGHTNESS, ATTR_RGB_COLOR, ENTITY_ID_FORMAT, - SUPPORT_BRIGHTNESS, SUPPORT_RGB_COLOR, Light) + ATTR_BRIGHTNESS, ATTR_HS_COLOR, ENTITY_ID_FORMAT, + SUPPORT_BRIGHTNESS, SUPPORT_COLOR, Light) from homeassistant.components.vera import ( VERA_CONTROLLER, VERA_DEVICES, VeraDevice) +import homeassistant.util.color as color_util _LOGGER = logging.getLogger(__name__) @@ -42,7 +43,7 @@ class VeraLight(VeraDevice, Light): return self._brightness @property - def rgb_color(self): + def hs_color(self): """Return the color of the light.""" return self._color @@ -50,13 +51,14 @@ class VeraLight(VeraDevice, Light): def supported_features(self): """Flag supported features.""" if self._color: - return SUPPORT_BRIGHTNESS | SUPPORT_RGB_COLOR + return SUPPORT_BRIGHTNESS | SUPPORT_COLOR return SUPPORT_BRIGHTNESS def turn_on(self, **kwargs): """Turn the light on.""" - if ATTR_RGB_COLOR in kwargs and self._color: - self.vera_device.set_color(kwargs[ATTR_RGB_COLOR]) + if ATTR_HS_COLOR in kwargs and self._color: + rgb = color_util.color_hs_to_RGB(*kwargs[ATTR_HS_COLOR]) + self.vera_device.set_color(rgb) elif ATTR_BRIGHTNESS in kwargs and self.vera_device.is_dimmable: self.vera_device.set_brightness(kwargs[ATTR_BRIGHTNESS]) else: @@ -83,4 +85,5 @@ class VeraLight(VeraDevice, Light): # If it is dimmable, both functions exist. In case color # is not supported, it will return None self._brightness = self.vera_device.get_brightness() - self._color = self.vera_device.get_color() + rgb = self.vera_device.get_color() + self._color = color_util.color_RGB_to_hs(*rgb) if rgb else None diff --git a/homeassistant/components/light/wemo.py b/homeassistant/components/light/wemo.py index 540c718b04d..d0575105235 100644 --- a/homeassistant/components/light/wemo.py +++ b/homeassistant/components/light/wemo.py @@ -9,12 +9,11 @@ import logging from datetime import timedelta import homeassistant.util as util -import homeassistant.util.color as color_util from homeassistant.components.light import ( - Light, ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_RGB_COLOR, ATTR_TRANSITION, - ATTR_XY_COLOR, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_RGB_COLOR, - SUPPORT_TRANSITION, SUPPORT_XY_COLOR) + Light, ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, ATTR_TRANSITION, + SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_COLOR, SUPPORT_TRANSITION) from homeassistant.loader import get_component +import homeassistant.util.color as color_util DEPENDENCIES = ['wemo'] @@ -23,8 +22,8 @@ MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(milliseconds=100) _LOGGER = logging.getLogger(__name__) -SUPPORT_WEMO = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_RGB_COLOR | - SUPPORT_TRANSITION | SUPPORT_XY_COLOR) +SUPPORT_WEMO = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_COLOR | + SUPPORT_TRANSITION) def setup_platform(hass, config, add_devices, discovery_info=None): @@ -89,9 +88,10 @@ class WemoLight(Light): return self.device.state.get('level', 255) @property - def xy_color(self): - """Return the XY color values of this light.""" - return self.device.state.get('color_xy') + def hs_color(self): + """Return the hs color values of this light.""" + xy_color = self.device.state.get('color_xy') + return color_util.color_xy_to_hs(*xy_color) if xy_color else None @property def color_temp(self): @@ -112,17 +112,11 @@ class WemoLight(Light): """Turn the light on.""" transitiontime = int(kwargs.get(ATTR_TRANSITION, 0)) - if ATTR_XY_COLOR in kwargs: - xycolor = kwargs[ATTR_XY_COLOR] - elif ATTR_RGB_COLOR in kwargs: - xycolor = color_util.color_RGB_to_xy( - *(int(val) for val in kwargs[ATTR_RGB_COLOR])) - kwargs.setdefault(ATTR_BRIGHTNESS, xycolor[2]) - else: - xycolor = None + hs_color = kwargs.get(ATTR_HS_COLOR) - if xycolor is not None: - self.device.set_color(xycolor, transition=transitiontime) + if hs_color is not None: + xy_color = color_util.color_hs_to_xy(*hs_color) + self.device.set_color(xy_color, transition=transitiontime) if ATTR_COLOR_TEMP in kwargs: colortemp = kwargs[ATTR_COLOR_TEMP] diff --git a/homeassistant/components/light/wink.py b/homeassistant/components/light/wink.py index e329fa04837..fd957f8f11d 100644 --- a/homeassistant/components/light/wink.py +++ b/homeassistant/components/light/wink.py @@ -8,8 +8,8 @@ import asyncio import colorsys from homeassistant.components.light import ( - ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_RGB_COLOR, SUPPORT_BRIGHTNESS, - SUPPORT_COLOR_TEMP, SUPPORT_RGB_COLOR, Light) + ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, SUPPORT_BRIGHTNESS, + SUPPORT_COLOR_TEMP, SUPPORT_COLOR, Light) from homeassistant.components.wink import DOMAIN, WinkDevice from homeassistant.util import color as color_util from homeassistant.util.color import \ @@ -17,7 +17,7 @@ from homeassistant.util.color import \ DEPENDENCIES = ['wink'] -SUPPORT_WINK = SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_RGB_COLOR +SUPPORT_WINK = SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_COLOR def setup_platform(hass, config, add_devices, discovery_info=None): @@ -72,11 +72,11 @@ class WinkLight(WinkDevice, Light): return r_value, g_value, b_value @property - def xy_color(self): - """Define current bulb color in CIE 1931 (XY) color space.""" + def hs_color(self): + """Define current bulb color.""" if not self.wink.supports_xy_color(): return None - return self.wink.color_xy() + return color_util.color_xy_to_hs(*self.wink.color_xy()) @property def color_temp(self): @@ -94,21 +94,17 @@ class WinkLight(WinkDevice, Light): def turn_on(self, **kwargs): """Turn the switch on.""" brightness = kwargs.get(ATTR_BRIGHTNESS) - rgb_color = kwargs.get(ATTR_RGB_COLOR) + hs_color = kwargs.get(ATTR_HS_COLOR) color_temp_mired = kwargs.get(ATTR_COLOR_TEMP) - state_kwargs = { - } + state_kwargs = {} - if rgb_color: + if hs_color: if self.wink.supports_xy_color(): - xyb = color_util.color_RGB_to_xy(*rgb_color) - state_kwargs['color_xy'] = xyb[0], xyb[1] - state_kwargs['brightness'] = xyb[2] + xy_color = color_util.color_hs_to_xy(*hs_color) + state_kwargs['color_xy'] = xy_color if self.wink.supports_hue_saturation(): - hsv = colorsys.rgb_to_hsv( - rgb_color[0], rgb_color[1], rgb_color[2]) - state_kwargs['color_hue_saturation'] = hsv[0], hsv[1] + state_kwargs['color_hue_saturation'] = hs_color if color_temp_mired: state_kwargs['color_kelvin'] = mired_to_kelvin(color_temp_mired) diff --git a/homeassistant/components/light/xiaomi_aqara.py b/homeassistant/components/light/xiaomi_aqara.py index efe37d3d577..125e791829f 100644 --- a/homeassistant/components/light/xiaomi_aqara.py +++ b/homeassistant/components/light/xiaomi_aqara.py @@ -4,9 +4,10 @@ import struct import binascii from homeassistant.components.xiaomi_aqara import (PY_XIAOMI_GATEWAY, XiaomiDevice) -from homeassistant.components.light import (ATTR_BRIGHTNESS, ATTR_RGB_COLOR, +from homeassistant.components.light import (ATTR_BRIGHTNESS, ATTR_HS_COLOR, SUPPORT_BRIGHTNESS, - SUPPORT_RGB_COLOR, Light) + SUPPORT_COLOR, Light) +import homeassistant.util.color as color_util _LOGGER = logging.getLogger(__name__) @@ -29,7 +30,7 @@ class XiaomiGatewayLight(XiaomiDevice, Light): def __init__(self, device, name, xiaomi_hub): """Initialize the XiaomiGatewayLight.""" self._data_key = 'rgb' - self._rgb = (255, 255, 255) + self._hs = (0, 0) self._brightness = 180 XiaomiDevice.__init__(self, device, name, xiaomi_hub) @@ -64,7 +65,7 @@ class XiaomiGatewayLight(XiaomiDevice, Light): rgb = rgba[1:] self._brightness = int(255 * brightness / 100) - self._rgb = rgb + self._hs = color_util.color_RGB_to_hs(*rgb) self._state = True return True @@ -74,24 +75,25 @@ class XiaomiGatewayLight(XiaomiDevice, Light): return self._brightness @property - def rgb_color(self): - """Return the RBG color value.""" - return self._rgb + def hs_color(self): + """Return the hs color value.""" + return self._hs @property def supported_features(self): """Return the supported features.""" - return SUPPORT_BRIGHTNESS | SUPPORT_RGB_COLOR + return SUPPORT_BRIGHTNESS | SUPPORT_COLOR def turn_on(self, **kwargs): """Turn the light on.""" - if ATTR_RGB_COLOR in kwargs: - self._rgb = kwargs[ATTR_RGB_COLOR] + if ATTR_HS_COLOR in kwargs: + self._hs = kwargs[ATTR_HS_COLOR] if ATTR_BRIGHTNESS in kwargs: self._brightness = int(100 * kwargs[ATTR_BRIGHTNESS] / 255) - rgba = (self._brightness,) + self._rgb + rgb = color_util.color_hs_to_RGB(*self._hs) + rgba = (self._brightness,) + rgb rgbhex = binascii.hexlify(struct.pack('BBBB', *rgba)).decode("ASCII") rgbhex = int(rgbhex, 16) diff --git a/homeassistant/components/light/yeelight.py b/homeassistant/components/light/yeelight.py index ca10d246ce8..585db950efc 100644 --- a/homeassistant/components/light/yeelight.py +++ b/homeassistant/components/light/yeelight.py @@ -5,26 +5,20 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/light.yeelight/ """ import logging -import colorsys -from typing import Tuple import voluptuous as vol from homeassistant.util.color import ( color_temperature_mired_to_kelvin as mired_to_kelvin, - color_temperature_kelvin_to_mired as kelvin_to_mired, - color_temperature_to_rgb, - color_RGB_to_xy, - color_xy_brightness_to_RGB) + color_temperature_kelvin_to_mired as kelvin_to_mired) from homeassistant.const import CONF_DEVICES, CONF_NAME from homeassistant.components.light import ( - ATTR_BRIGHTNESS, ATTR_RGB_COLOR, ATTR_TRANSITION, ATTR_COLOR_TEMP, - ATTR_FLASH, ATTR_XY_COLOR, FLASH_SHORT, FLASH_LONG, ATTR_EFFECT, - SUPPORT_BRIGHTNESS, SUPPORT_RGB_COLOR, SUPPORT_XY_COLOR, - SUPPORT_TRANSITION, - SUPPORT_COLOR_TEMP, SUPPORT_FLASH, SUPPORT_EFFECT, - Light, PLATFORM_SCHEMA) + ATTR_BRIGHTNESS, ATTR_HS_COLOR, ATTR_TRANSITION, ATTR_COLOR_TEMP, + ATTR_FLASH, FLASH_SHORT, FLASH_LONG, ATTR_EFFECT, SUPPORT_BRIGHTNESS, + SUPPORT_COLOR, SUPPORT_TRANSITION, SUPPORT_COLOR_TEMP, SUPPORT_FLASH, + SUPPORT_EFFECT, Light, PLATFORM_SCHEMA) import homeassistant.helpers.config_validation as cv +import homeassistant.util.color as color_util REQUIREMENTS = ['yeelight==0.4.0'] @@ -53,8 +47,7 @@ SUPPORT_YEELIGHT = (SUPPORT_BRIGHTNESS | SUPPORT_FLASH) SUPPORT_YEELIGHT_RGB = (SUPPORT_YEELIGHT | - SUPPORT_RGB_COLOR | - SUPPORT_XY_COLOR | + SUPPORT_COLOR | SUPPORT_EFFECT | SUPPORT_COLOR_TEMP) @@ -98,14 +91,6 @@ YEELIGHT_EFFECT_LIST = [ EFFECT_STOP] -# Travis-CI runs too old astroid https://github.com/PyCQA/pylint/issues/1212 -# pylint: disable=invalid-sequence-index -def hsv_to_rgb(hsv: Tuple[float, float, float]) -> Tuple[int, int, int]: - """Convert HSV tuple (degrees, %, %) to RGB (values 0-255).""" - red, green, blue = colorsys.hsv_to_rgb(hsv[0]/360, hsv[1]/100, hsv[2]/100) - return int(red * 255), int(green * 255), int(blue * 255) - - def _cmd(func): """Define a wrapper to catch exceptions from the bulb.""" def _wrap(self, *args, **kwargs): @@ -157,8 +142,7 @@ class YeelightLight(Light): self._brightness = None self._color_temp = None self._is_on = None - self._rgb = None - self._xy = None + self._hs = None @property def available(self) -> bool: @@ -209,38 +193,32 @@ class YeelightLight(Light): return kelvin_to_mired(YEELIGHT_RGB_MIN_KELVIN) return kelvin_to_mired(YEELIGHT_MIN_KELVIN) - def _get_rgb_from_properties(self): + def _get_hs_from_properties(self): rgb = self._properties.get('rgb', None) color_mode = self._properties.get('color_mode', None) if not rgb or not color_mode: - return rgb + return None color_mode = int(color_mode) if color_mode == 2: # color temperature temp_in_k = mired_to_kelvin(self._color_temp) - return color_temperature_to_rgb(temp_in_k) + return color_util.color_temperature_to_hs(temp_in_k) if color_mode == 3: # hsv hue = int(self._properties.get('hue')) sat = int(self._properties.get('sat')) - val = int(self._properties.get('bright')) - return hsv_to_rgb((hue, sat, val)) + return (hue / 360 * 65536, sat / 100 * 255) rgb = int(rgb) blue = rgb & 0xff green = (rgb >> 8) & 0xff red = (rgb >> 16) & 0xff - return red, green, blue + return color_util.color_RGB_to_hs(red, green, blue) @property - def rgb_color(self) -> tuple: + def hs_color(self) -> tuple: """Return the color property.""" - return self._rgb - - @property - def xy_color(self) -> tuple: - """Return the XY color value.""" - return self._xy + return self._hs @property def _properties(self) -> dict: @@ -288,13 +266,7 @@ class YeelightLight(Light): if temp_in_k: self._color_temp = kelvin_to_mired(int(temp_in_k)) - self._rgb = self._get_rgb_from_properties() - - if self._rgb: - xyb = color_RGB_to_xy(*self._rgb) - self._xy = (xyb[0], xyb[1]) - else: - self._xy = None + self._hs = self._get_hs_from_properties() self._available = True except yeelight.BulbException as ex: @@ -313,7 +285,7 @@ class YeelightLight(Light): @_cmd def set_rgb(self, rgb, duration) -> None: """Set bulb's color.""" - if rgb and self.supported_features & SUPPORT_RGB_COLOR: + if rgb and self.supported_features & SUPPORT_COLOR: _LOGGER.debug("Setting RGB: %s", rgb) self._bulb.set_rgb(rgb[0], rgb[1], rgb[2], duration=duration) @@ -349,7 +321,7 @@ class YeelightLight(Light): count = 1 duration = transition * 2 - red, green, blue = self.rgb_color + red, green, blue = color_util.color_hs_to_RGB(*self._hs) transitions = list() transitions.append( @@ -419,10 +391,10 @@ class YeelightLight(Light): import yeelight brightness = kwargs.get(ATTR_BRIGHTNESS) colortemp = kwargs.get(ATTR_COLOR_TEMP) - rgb = kwargs.get(ATTR_RGB_COLOR) + hs_color = kwargs.get(ATTR_HS_COLOR) + rgb = color_util.color_hs_to_RGB(*hs_color) if hs_color else None flash = kwargs.get(ATTR_FLASH) effect = kwargs.get(ATTR_EFFECT) - xy_color = kwargs.get(ATTR_XY_COLOR) duration = int(self.config[CONF_TRANSITION]) # in ms if ATTR_TRANSITION in kwargs: # passed kwarg overrides config @@ -440,9 +412,6 @@ class YeelightLight(Light): except yeelight.BulbException as ex: _LOGGER.error("Unable to turn on music mode," "consider disabling it: %s", ex) - if xy_color and brightness: - rgb = color_xy_brightness_to_RGB(xy_color[0], xy_color[1], - brightness) try: # values checked for none in methods diff --git a/homeassistant/components/light/yeelightsunflower.py b/homeassistant/components/light/yeelightsunflower.py index 5f48e3a0a71..88f86063c13 100644 --- a/homeassistant/components/light/yeelightsunflower.py +++ b/homeassistant/components/light/yeelightsunflower.py @@ -10,15 +10,16 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.components.light import ( - Light, ATTR_RGB_COLOR, SUPPORT_RGB_COLOR, ATTR_BRIGHTNESS, + Light, ATTR_HS_COLOR, SUPPORT_COLOR, ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, PLATFORM_SCHEMA) from homeassistant.const import CONF_HOST +import homeassistant.util.color as color_util REQUIREMENTS = ['yeelightsunflower==0.0.8'] _LOGGER = logging.getLogger(__name__) -SUPPORT_YEELIGHT_SUNFLOWER = (SUPPORT_BRIGHTNESS | SUPPORT_RGB_COLOR) +SUPPORT_YEELIGHT_SUNFLOWER = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_HOST): cv.string @@ -48,7 +49,7 @@ class SunflowerBulb(Light): self._available = light.available self._brightness = light.brightness self._is_on = light.is_on - self._rgb_color = light.rgb_color + self._hs_color = light.rgb_color @property def name(self): @@ -71,9 +72,9 @@ class SunflowerBulb(Light): return int(self._brightness / 100 * 255) @property - def rgb_color(self): + def hs_color(self): """Return the color property.""" - return self._rgb_color + return self._hs_color @property def supported_features(self): @@ -86,12 +87,12 @@ class SunflowerBulb(Light): if not kwargs: self._light.turn_on() else: - if ATTR_RGB_COLOR in kwargs and ATTR_BRIGHTNESS in kwargs: - rgb = kwargs[ATTR_RGB_COLOR] + if ATTR_HS_COLOR in kwargs and ATTR_BRIGHTNESS in kwargs: + rgb = color_util.color_hs_to_RGB(*kwargs[ATTR_HS_COLOR]) bright = int(kwargs[ATTR_BRIGHTNESS] / 255 * 100) self._light.set_all(rgb[0], rgb[1], rgb[2], bright) - elif ATTR_RGB_COLOR in kwargs: - rgb = kwargs[ATTR_RGB_COLOR] + elif ATTR_HS_COLOR in kwargs: + rgb = color_util.color_hs_to_RGB(*kwargs[ATTR_HS_COLOR]) self._light.set_rgb_color(rgb[0], rgb[1], rgb[2]) elif ATTR_BRIGHTNESS in kwargs: bright = int(kwargs[ATTR_BRIGHTNESS] / 255 * 100) @@ -107,4 +108,4 @@ class SunflowerBulb(Light): self._available = self._light.available self._brightness = self._light.brightness self._is_on = self._light.is_on - self._rgb_color = self._light.rgb_color + self._hs_color = color_util.color_RGB_to_hs(*self._light.rgb_color) diff --git a/homeassistant/components/light/zengge.py b/homeassistant/components/light/zengge.py index 7071c8c43bb..3c77f2d8449 100644 --- a/homeassistant/components/light/zengge.py +++ b/homeassistant/components/light/zengge.py @@ -10,15 +10,16 @@ import voluptuous as vol from homeassistant.const import CONF_DEVICES, CONF_NAME from homeassistant.components.light import ( - ATTR_RGB_COLOR, ATTR_WHITE_VALUE, - SUPPORT_RGB_COLOR, SUPPORT_WHITE_VALUE, Light, PLATFORM_SCHEMA) + ATTR_BRIGHTNESS, ATTR_HS_COLOR, ATTR_WHITE_VALUE, SUPPORT_BRIGHTNESS, + SUPPORT_COLOR, SUPPORT_WHITE_VALUE, Light, PLATFORM_SCHEMA) import homeassistant.helpers.config_validation as cv +import homeassistant.util.color as color_util REQUIREMENTS = ['zengge==0.2'] _LOGGER = logging.getLogger(__name__) -SUPPORT_ZENGGE_LED = (SUPPORT_RGB_COLOR | SUPPORT_WHITE_VALUE) +SUPPORT_ZENGGE_LED = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR | SUPPORT_WHITE_VALUE) DEVICE_SCHEMA = vol.Schema({ vol.Optional(CONF_NAME): cv.string, @@ -56,7 +57,8 @@ class ZenggeLight(Light): self.is_valid = True self._bulb = zengge.zengge(self._address) self._white = 0 - self._rgb = (0, 0, 0) + self._brightness = 0 + self._hs_color = (0, 0) self._state = False if self._bulb.connect() is False: self.is_valid = False @@ -80,9 +82,14 @@ class ZenggeLight(Light): return self._state @property - def rgb_color(self): + def brightness(self): + """Return the brightness property.""" + return self._brightness + + @property + def hs_color(self): """Return the color property.""" - return self._rgb + return self._hs_color @property def white_value(self): @@ -117,21 +124,29 @@ class ZenggeLight(Light): self._state = True self._bulb.on() - rgb = kwargs.get(ATTR_RGB_COLOR) + hs_color = kwargs.get(ATTR_HS_COLOR) white = kwargs.get(ATTR_WHITE_VALUE) + brightness = kwargs.get(ATTR_BRIGHTNESS) if white is not None: self._white = white - self._rgb = (0, 0, 0) + self._hs_color = (0, 0) - if rgb is not None: + if hs_color is not None: self._white = 0 - self._rgb = rgb + self._hs_color = hs_color + + if brightness is not None: + self._white = 0 + self._brightness = brightness if self._white != 0: self.set_white(self._white) else: - self.set_rgb(self._rgb[0], self._rgb[1], self._rgb[2]) + rgb = color_util.color_hsv_to_RGB( + self._hs_color[0], self._hs_color[1], + self._brightness / 255 * 100) + self.set_rgb(*rgb) def turn_off(self, **kwargs): """Turn the specified light off.""" @@ -140,6 +155,9 @@ class ZenggeLight(Light): def update(self): """Synchronise internal state with the actual light state.""" - self._rgb = self._bulb.get_colour() + rgb = self._bulb.get_colour() + hsv = color_util.color_RGB_to_hsv(*rgb) + self._hs_color = hsv[:2] + self._brightness = hsv[2] self._white = self._bulb.get_white() self._state = self._bulb.get_on() diff --git a/homeassistant/components/light/zha.py b/homeassistant/components/light/zha.py index 7958fcabf13..68c5bcc2a29 100644 --- a/homeassistant/components/light/zha.py +++ b/homeassistant/components/light/zha.py @@ -7,8 +7,8 @@ at https://home-assistant.io/components/light.zha/ import logging from homeassistant.components import light, zha -from homeassistant.util.color import color_RGB_to_xy from homeassistant.const import STATE_UNKNOWN +import homeassistant.util.color as color_util _LOGGER = logging.getLogger(__name__) @@ -57,7 +57,7 @@ class Light(zha.Entity, light.Light): super().__init__(**kwargs) self._supported_features = 0 self._color_temp = None - self._xy_color = None + self._hs_color = None self._brightness = None import zigpy.zcl.clusters as zcl_clusters @@ -71,9 +71,8 @@ class Light(zha.Entity, light.Light): self._supported_features |= light.SUPPORT_COLOR_TEMP if color_capabilities & CAPABILITIES_COLOR_XY: - self._supported_features |= light.SUPPORT_XY_COLOR - self._supported_features |= light.SUPPORT_RGB_COLOR - self._xy_color = (1.0, 1.0) + self._supported_features |= light.SUPPORT_COLOR + self._hs_color = (0, 0) @property def is_on(self) -> bool: @@ -92,17 +91,12 @@ class Light(zha.Entity, light.Light): temperature, duration) self._color_temp = temperature - if light.ATTR_XY_COLOR in kwargs: - self._xy_color = kwargs[light.ATTR_XY_COLOR] - elif light.ATTR_RGB_COLOR in kwargs: - xyb = color_RGB_to_xy( - *(int(val) for val in kwargs[light.ATTR_RGB_COLOR])) - self._xy_color = (xyb[0], xyb[1]) - self._brightness = xyb[2] - if light.ATTR_XY_COLOR in kwargs or light.ATTR_RGB_COLOR in kwargs: + if light.ATTR_HS_COLOR in kwargs: + self._hs_color = kwargs[light.ATTR_HS_COLOR] + xy_color = color_util.color_hs_to_xy(*self._hs_color) await self._endpoint.light_color.move_to_color( - int(self._xy_color[0] * 65535), - int(self._xy_color[1] * 65535), + int(xy_color[0] * 65535), + int(xy_color[1] * 65535), duration, ) @@ -135,9 +129,9 @@ class Light(zha.Entity, light.Light): return self._brightness @property - def xy_color(self): - """Return the XY color value [float, float].""" - return self._xy_color + def hs_color(self): + """Return the hs color value [int, int].""" + return self._hs_color @property def color_temp(self): @@ -165,11 +159,12 @@ class Light(zha.Entity, light.Light): self._color_temp = result.get('color_temperature', self._color_temp) - if self._supported_features & light.SUPPORT_XY_COLOR: + if self._supported_features & light.SUPPORT_COLOR: result = await zha.safe_read(self._endpoint.light_color, ['current_x', 'current_y']) if 'current_x' in result and 'current_y' in result: - self._xy_color = (result['current_x'], result['current_y']) + xy_color = (result['current_x'], result['current_y']) + self._hs_color = color_util.color_xy_to_hs(*xy_color) @property def should_poll(self) -> bool: diff --git a/homeassistant/components/light/zwave.py b/homeassistant/components/light/zwave.py index 64c6530dd2b..286ce73f1ed 100644 --- a/homeassistant/components/light/zwave.py +++ b/homeassistant/components/light/zwave.py @@ -9,14 +9,14 @@ import logging # Because we do not compile openzwave on CI # pylint: disable=import-error from threading import Timer -from homeassistant.components.light import ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, \ - ATTR_RGB_COLOR, ATTR_TRANSITION, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, \ - SUPPORT_RGB_COLOR, SUPPORT_TRANSITION, DOMAIN, Light +from homeassistant.components.light import ( + ATTR_WHITE_VALUE, ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, + ATTR_TRANSITION, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_COLOR, + SUPPORT_TRANSITION, SUPPORT_WHITE_VALUE, DOMAIN, Light) from homeassistant.components import zwave from homeassistant.components.zwave import async_setup_platform # noqa # pylint: disable=unused-import from homeassistant.const import STATE_OFF, STATE_ON -from homeassistant.util.color import color_temperature_mired_to_kelvin, \ - color_temperature_to_rgb, color_rgb_to_rgbw, color_rgbw_to_rgb +import homeassistant.util.color as color_util _LOGGER = logging.getLogger(__name__) @@ -65,10 +65,11 @@ def brightness_state(value): return 0, STATE_OFF -def ct_to_rgb(temp): - """Convert color temperature (mireds) to RGB.""" +def ct_to_hs(temp): + """Convert color temperature (mireds) to hs.""" colorlist = list( - color_temperature_to_rgb(color_temperature_mired_to_kelvin(temp))) + color_util.color_temperature_to_hs( + color_util.color_temperature_mired_to_kelvin(temp))) return [int(val) for val in colorlist] @@ -209,8 +210,9 @@ class ZwaveColorLight(ZwaveDimmer): def __init__(self, values, refresh, delay): """Initialize the light.""" self._color_channels = None - self._rgb = None + self._hs = None self._ct = None + self._white = None super().__init__(values, refresh, delay) @@ -218,9 +220,12 @@ class ZwaveColorLight(ZwaveDimmer): """Call when a new value is added to this entity.""" super().value_added() - self._supported_features |= SUPPORT_RGB_COLOR + self._supported_features |= SUPPORT_COLOR if self._zw098: self._supported_features |= SUPPORT_COLOR_TEMP + elif self._color_channels is not None and self._color_channels & ( + COLOR_CHANNEL_WARM_WHITE | COLOR_CHANNEL_COLD_WHITE): + self._supported_features |= SUPPORT_WHITE_VALUE def update_properties(self): """Update internal properties based on zwave values.""" @@ -238,10 +243,11 @@ class ZwaveColorLight(ZwaveDimmer): data = self.values.color.data # RGB is always present in the openzwave color data string. - self._rgb = [ + rgb = [ int(data[1:3], 16), int(data[3:5], 16), int(data[5:7], 16)] + self._hs = color_util.color_RGB_to_hs(*rgb) # Parse remaining color channels. Openzwave appends white channels # that are present. @@ -267,30 +273,35 @@ class ZwaveColorLight(ZwaveDimmer): if self._zw098: if warm_white > 0: self._ct = TEMP_WARM_HASS - self._rgb = ct_to_rgb(self._ct) + self._hs = ct_to_hs(self._ct) elif cold_white > 0: self._ct = TEMP_COLD_HASS - self._rgb = ct_to_rgb(self._ct) + self._hs = ct_to_hs(self._ct) else: # RGB color is being used. Just report midpoint. self._ct = TEMP_MID_HASS elif self._color_channels & COLOR_CHANNEL_WARM_WHITE: - self._rgb = list(color_rgbw_to_rgb(*self._rgb, w=warm_white)) + self._white = warm_white elif self._color_channels & COLOR_CHANNEL_COLD_WHITE: - self._rgb = list(color_rgbw_to_rgb(*self._rgb, w=cold_white)) + self._white = cold_white # If no rgb channels supported, report None. if not (self._color_channels & COLOR_CHANNEL_RED or self._color_channels & COLOR_CHANNEL_GREEN or self._color_channels & COLOR_CHANNEL_BLUE): - self._rgb = None + self._hs = None @property - def rgb_color(self): - """Return the rgb color.""" - return self._rgb + def hs_color(self): + """Return the hs color.""" + return self._hs + + @property + def white_value(self): + """Return the white value of this light between 0..255.""" + return self._white @property def color_temp(self): @@ -301,6 +312,9 @@ class ZwaveColorLight(ZwaveDimmer): """Turn the device on.""" rgbw = None + if ATTR_WHITE_VALUE in kwargs: + self._white = kwargs[ATTR_WHITE_VALUE] + if ATTR_COLOR_TEMP in kwargs: # Color temperature. With the AEOTEC ZW098 bulb, only two color # temperatures are supported. The warm and cold channel values @@ -313,19 +327,16 @@ class ZwaveColorLight(ZwaveDimmer): self._ct = TEMP_COLD_HASS rgbw = '#00000000ff' - elif ATTR_RGB_COLOR in kwargs: - self._rgb = kwargs[ATTR_RGB_COLOR] - if (not self._zw098 and ( - self._color_channels & COLOR_CHANNEL_WARM_WHITE or - self._color_channels & COLOR_CHANNEL_COLD_WHITE)): - rgbw = '#' - for colorval in color_rgb_to_rgbw(*self._rgb): - rgbw += format(colorval, '02x') - rgbw += '00' + elif ATTR_HS_COLOR in kwargs: + self._hs = kwargs[ATTR_HS_COLOR] + + if ATTR_WHITE_VALUE in kwargs or ATTR_HS_COLOR in kwargs: + rgbw = '#' + for colorval in color_util.color_hs_to_RGB(*self._hs): + rgbw += format(colorval, '02x') + if self._white is not None: + rgbw += format(self._white, '02x') + '00' else: - rgbw = '#' - for colorval in self._rgb: - rgbw += format(colorval, '02x') rgbw += '0000' if rgbw and self.values.color: diff --git a/homeassistant/components/switch/flux.py b/homeassistant/components/switch/flux.py index acc0c3ac423..e0bfdeee030 100644 --- a/homeassistant/components/switch/flux.py +++ b/homeassistant/components/switch/flux.py @@ -21,7 +21,7 @@ from homeassistant.helpers.event import track_time_change from homeassistant.helpers.sun import get_astral_event_date from homeassistant.util import slugify from homeassistant.util.color import ( - color_temperature_to_rgb, color_RGB_to_xy, + color_temperature_to_rgb, color_RGB_to_xy_brightness, color_temperature_kelvin_to_mired) from homeassistant.util.dt import now as dt_now @@ -234,7 +234,7 @@ class FluxSwitch(SwitchDevice): else: temp = self._sunset_colortemp + temp_offset rgb = color_temperature_to_rgb(temp) - x_val, y_val, b_val = color_RGB_to_xy(*rgb) + x_val, y_val, b_val = color_RGB_to_xy_brightness(*rgb) brightness = self._brightness if self._brightness else b_val if self._disable_brightness_adjust: brightness = None diff --git a/homeassistant/util/color.py b/homeassistant/util/color.py index 70863a0ab90..c2e4ac737e8 100644 --- a/homeassistant/util/color.py +++ b/homeassistant/util/color.py @@ -173,11 +173,18 @@ def color_name_to_rgb(color_name): return hex_value +# pylint: disable=invalid-name, invalid-sequence-index +def color_RGB_to_xy(iR: int, iG: int, iB: int) -> Tuple[float, float]: + """Convert from RGB color to XY color.""" + return color_RGB_to_xy_brightness(iR, iG, iB)[:2] + + # Taken from: # http://www.developers.meethue.com/documentation/color-conversions-rgb-xy # License: Code is given as is. Use at your own risk and discretion. # pylint: disable=invalid-name, invalid-sequence-index -def color_RGB_to_xy(iR: int, iG: int, iB: int) -> Tuple[float, float, int]: +def color_RGB_to_xy_brightness( + iR: int, iG: int, iB: int) -> Tuple[float, float, int]: """Convert from RGB color to XY color.""" if iR + iG + iB == 0: return 0.0, 0.0, 0 @@ -210,6 +217,11 @@ def color_RGB_to_xy(iR: int, iG: int, iB: int) -> Tuple[float, float, int]: return round(x, 3), round(y, 3), brightness +def color_xy_to_RGB(vX: float, vY: float) -> Tuple[int, int, int]: + """Convert from XY to a normalized RGB.""" + return color_xy_brightness_to_RGB(vX, vY, 255) + + # Converted to Python from Obj-C, original source from: # http://www.developers.meethue.com/documentation/color-conversions-rgb-xy # pylint: disable=invalid-sequence-index @@ -307,6 +319,12 @@ def color_RGB_to_hsv(iR: int, iG: int, iB: int) -> Tuple[float, float, float]: return round(fHSV[0]*360, 3), round(fHSV[1]*100, 3), round(fHSV[2]*100, 3) +# pylint: disable=invalid-sequence-index +def color_RGB_to_hs(iR: int, iG: int, iB: int) -> Tuple[float, float]: + """Convert an rgb color to its hs representation.""" + return color_RGB_to_hsv(iR, iG, iB)[:2] + + # pylint: disable=invalid-sequence-index def color_hsv_to_RGB(iH: float, iS: float, iV: float) -> Tuple[int, int, int]: """Convert an hsv color into its rgb representation. @@ -320,12 +338,24 @@ def color_hsv_to_RGB(iH: float, iS: float, iV: float) -> Tuple[int, int, int]: # pylint: disable=invalid-sequence-index -def color_xy_to_hs(vX: float, vY: float) -> Tuple[int, int]: +def color_hs_to_RGB(iH: float, iS: float) -> Tuple[int, int, int]: + """Convert an hsv color into its rgb representation.""" + return color_hsv_to_RGB(iH, iS, 100) + + +# pylint: disable=invalid-sequence-index +def color_xy_to_hs(vX: float, vY: float) -> Tuple[float, float]: """Convert an xy color to its hs representation.""" - h, s, _ = color_RGB_to_hsv(*color_xy_brightness_to_RGB(vX, vY, 255)) + h, s, _ = color_RGB_to_hsv(*color_xy_to_RGB(vX, vY)) return (h, s) +# pylint: disable=invalid-sequence-index +def color_hs_to_xy(iH: float, iS: float) -> Tuple[float, float]: + """Convert an hs color to its xy representation.""" + return color_RGB_to_xy(*color_hs_to_RGB(iH, iS)) + + # pylint: disable=invalid-sequence-index def _match_max_scale(input_colors: Tuple[int, ...], output_colors: Tuple[int, ...]) -> Tuple[int, ...]: @@ -374,6 +404,11 @@ def rgb_hex_to_rgb_list(hex_string): len(hex_string) // 3)] +def color_temperature_to_hs(color_temperature_kelvin): + """Return an hs color from a color temperature in Kelvin.""" + return color_RGB_to_hs(*color_temperature_to_rgb(color_temperature_kelvin)) + + def color_temperature_to_rgb(color_temperature_kelvin): """ Return an RGB color from a color temperature in Kelvin. diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index a5375ba2662..8199652d09e 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -950,42 +950,6 @@ def test_api_set_color_rgb(hass): assert msg['header']['name'] == 'Response' -@asyncio.coroutine -def test_api_set_color_xy(hass): - """Test api set color process.""" - request = get_new_request( - 'Alexa.ColorController', 'SetColor', 'light#test') - - # add payload - request['directive']['payload']['color'] = { - 'hue': '120', - 'saturation': '0.612', - 'brightness': '0.342', - } - - # setup test devices - hass.states.async_set( - 'light.test', 'off', { - 'friendly_name': "Test light", - 'supported_features': 64, - }) - - call_light = async_mock_service(hass, 'light', 'turn_on') - - msg = yield from smart_home.async_handle_message( - hass, DEFAULT_CONFIG, request) - yield from hass.async_block_till_done() - - assert 'event' in msg - msg = msg['event'] - - assert len(call_light) == 1 - assert call_light[0].data['entity_id'] == 'light.test' - assert call_light[0].data['xy_color'] == (0.23, 0.585) - assert call_light[0].data['brightness'] == 18 - assert msg['header']['name'] == 'Response' - - @asyncio.coroutine def test_api_set_color_temperature(hass): """Test api set color temperature process.""" diff --git a/tests/components/google_assistant/test_smart_home.py b/tests/components/google_assistant/test_smart_home.py index 24d74afa6da..6523c22fee1 100644 --- a/tests/components/google_assistant/test_smart_home.py +++ b/tests/components/google_assistant/test_smart_home.py @@ -21,7 +21,7 @@ async def test_sync_message(hass): light = DemoLight( None, 'Demo Light', state=False, - rgb=[237, 224, 33] + hs_color=(180, 75), ) light.hass = hass light.entity_id = 'light.demo_light' @@ -88,7 +88,7 @@ async def test_query_message(hass): light = DemoLight( None, 'Demo Light', state=False, - rgb=[237, 224, 33] + hs_color=(180, 75), ) light.hass = hass light.entity_id = 'light.demo_light' @@ -97,7 +97,7 @@ async def test_query_message(hass): light2 = DemoLight( None, 'Another Light', state=True, - rgb=[237, 224, 33], + hs_color=(180, 75), ct=400, brightness=78, ) @@ -137,7 +137,7 @@ async def test_query_message(hass): 'online': True, 'brightness': 30, 'color': { - 'spectrumRGB': 15589409, + 'spectrumRGB': 4194303, 'temperature': 2500, } }, @@ -197,7 +197,7 @@ async def test_execute(hass): "online": True, 'brightness': 20, 'color': { - 'spectrumRGB': 15589409, + 'spectrumRGB': 16773155, 'temperature': 2631, }, } diff --git a/tests/components/google_assistant/test_trait.py b/tests/components/google_assistant/test_trait.py index 4ffb273662e..e6336e05246 100644 --- a/tests/components/google_assistant/test_trait.py +++ b/tests/components/google_assistant/test_trait.py @@ -361,12 +361,10 @@ async def test_color_spectrum_light(hass): """Test ColorSpectrum trait support for light domain.""" assert not trait.ColorSpectrumTrait.supported(light.DOMAIN, 0) assert trait.ColorSpectrumTrait.supported(light.DOMAIN, - light.SUPPORT_RGB_COLOR) - assert trait.ColorSpectrumTrait.supported(light.DOMAIN, - light.SUPPORT_XY_COLOR) + light.SUPPORT_COLOR) trt = trait.ColorSpectrumTrait(State('light.bla', STATE_ON, { - light.ATTR_RGB_COLOR: [255, 10, 10] + light.ATTR_HS_COLOR: (0, 94), })) assert trt.sync_attributes() == { @@ -375,7 +373,7 @@ async def test_color_spectrum_light(hass): assert trt.query_attributes() == { 'color': { - 'spectrumRGB': 16714250 + 'spectrumRGB': 16715535 } } @@ -399,7 +397,7 @@ async def test_color_spectrum_light(hass): assert len(calls) == 1 assert calls[0].data == { ATTR_ENTITY_ID: 'light.bla', - light.ATTR_RGB_COLOR: [16, 16, 255] + light.ATTR_HS_COLOR: (240, 93.725), } diff --git a/tests/components/homekit/test_type_lights.py b/tests/components/homekit/test_type_lights.py index 83456f459cd..b4d4d5a5945 100644 --- a/tests/components/homekit/test_type_lights.py +++ b/tests/components/homekit/test_type_lights.py @@ -4,8 +4,8 @@ import unittest from homeassistant.core import callback from homeassistant.components.homekit.type_lights import Light from homeassistant.components.light import ( - DOMAIN, ATTR_BRIGHTNESS, ATTR_BRIGHTNESS_PCT, ATTR_RGB_COLOR, - SUPPORT_BRIGHTNESS, SUPPORT_RGB_COLOR) + DOMAIN, ATTR_BRIGHTNESS, ATTR_BRIGHTNESS_PCT, ATTR_HS_COLOR, + SUPPORT_BRIGHTNESS, SUPPORT_COLOR) from homeassistant.const import ( ATTR_DOMAIN, ATTR_ENTITY_ID, ATTR_SERVICE, ATTR_SERVICE_DATA, ATTR_SUPPORTED_FEATURES, EVENT_CALL_SERVICE, SERVICE_TURN_ON, @@ -108,16 +108,16 @@ class TestHomekitLights(unittest.TestCase): """Test light with rgb_color.""" entity_id = 'light.demo' self.hass.states.set(entity_id, STATE_ON, { - ATTR_SUPPORTED_FEATURES: SUPPORT_RGB_COLOR, - ATTR_RGB_COLOR: (120, 20, 255)}) + ATTR_SUPPORTED_FEATURES: SUPPORT_COLOR, + ATTR_HS_COLOR: (260, 90)}) acc = Light(self.hass, entity_id, 'Light', aid=2) self.assertEqual(acc.char_hue.value, 0) self.assertEqual(acc.char_saturation.value, 75) acc.run() self.hass.block_till_done() - self.assertEqual(acc.char_hue.value, 265.532) - self.assertEqual(acc.char_saturation.value, 92.157) + self.assertEqual(acc.char_hue.value, 260) + self.assertEqual(acc.char_saturation.value, 90) # Set from HomeKit acc.char_hue.set_value(145) @@ -129,4 +129,4 @@ class TestHomekitLights(unittest.TestCase): self.events[0].data[ATTR_SERVICE], SERVICE_TURN_ON) self.assertEqual( self.events[0].data[ATTR_SERVICE_DATA], { - ATTR_ENTITY_ID: entity_id, ATTR_RGB_COLOR: (63, 255, 143)}) + ATTR_ENTITY_ID: entity_id, ATTR_HS_COLOR: (145, 75)}) diff --git a/tests/components/light/test_demo.py b/tests/components/light/test_demo.py index 8a7d648e6f2..ff984aff221 100644 --- a/tests/components/light/test_demo.py +++ b/tests/components/light/test_demo.py @@ -33,19 +33,22 @@ class TestDemoLight(unittest.TestCase): self.hass.block_till_done() state = self.hass.states.get(ENTITY_LIGHT) self.assertTrue(light.is_on(self.hass, ENTITY_LIGHT)) - self.assertEqual((.4, .6), state.attributes.get(light.ATTR_XY_COLOR)) + self.assertEqual((0.378, 0.574), state.attributes.get( + light.ATTR_XY_COLOR)) self.assertEqual(25, state.attributes.get(light.ATTR_BRIGHTNESS)) self.assertEqual( - (76, 95, 0), state.attributes.get(light.ATTR_RGB_COLOR)) + (207, 255, 0), state.attributes.get(light.ATTR_RGB_COLOR)) self.assertEqual('rainbow', state.attributes.get(light.ATTR_EFFECT)) light.turn_on( - self.hass, ENTITY_LIGHT, rgb_color=(251, 252, 253), + self.hass, ENTITY_LIGHT, rgb_color=(251, 253, 255), white_value=254) self.hass.block_till_done() state = self.hass.states.get(ENTITY_LIGHT) self.assertEqual(254, state.attributes.get(light.ATTR_WHITE_VALUE)) self.assertEqual( - (251, 252, 253), state.attributes.get(light.ATTR_RGB_COLOR)) + (250, 252, 255), state.attributes.get(light.ATTR_RGB_COLOR)) + self.assertEqual( + (0.316, 0.333), state.attributes.get(light.ATTR_XY_COLOR)) light.turn_on(self.hass, ENTITY_LIGHT, color_temp=400, effect='none') self.hass.block_till_done() state = self.hass.states.get(ENTITY_LIGHT) diff --git a/tests/components/light/test_group.py b/tests/components/light/test_group.py index 3c94fa2af3e..26b949720d9 100644 --- a/tests/components/light/test_group.py +++ b/tests/components/light/test_group.py @@ -20,8 +20,7 @@ async def test_default_state(hass): assert state.state == 'unavailable' assert state.attributes['supported_features'] == 0 assert state.attributes.get('brightness') is None - assert state.attributes.get('rgb_color') is None - assert state.attributes.get('xy_color') is None + assert state.attributes.get('hs_color') is None assert state.attributes.get('color_temp') is None assert state.attributes.get('white_value') is None assert state.attributes.get('effect_list') is None @@ -85,61 +84,32 @@ async def test_brightness(hass): assert state.attributes['brightness'] == 100 -async def test_xy_color(hass): - """Test XY reporting.""" - await async_setup_component(hass, 'light', {'light': { - 'platform': 'group', 'entities': ['light.test1', 'light.test2'] - }}) - - hass.states.async_set('light.test1', 'on', - {'xy_color': (1.0, 1.0), 'supported_features': 64}) - await hass.async_block_till_done() - state = hass.states.get('light.light_group') - assert state.state == 'on' - assert state.attributes['supported_features'] == 64 - assert state.attributes['xy_color'] == (1.0, 1.0) - - hass.states.async_set('light.test2', 'on', - {'xy_color': (0.5, 0.5), 'supported_features': 64}) - await hass.async_block_till_done() - state = hass.states.get('light.light_group') - assert state.state == 'on' - assert state.attributes['xy_color'] == (0.75, 0.75) - - hass.states.async_set('light.test1', 'off', - {'xy_color': (1.0, 1.0), 'supported_features': 64}) - await hass.async_block_till_done() - state = hass.states.get('light.light_group') - assert state.state == 'on' - assert state.attributes['xy_color'] == (0.5, 0.5) - - -async def test_rgb_color(hass): +async def test_color(hass): """Test RGB reporting.""" await async_setup_component(hass, 'light', {'light': { 'platform': 'group', 'entities': ['light.test1', 'light.test2'] }}) hass.states.async_set('light.test1', 'on', - {'rgb_color': (255, 0, 0), 'supported_features': 16}) + {'hs_color': (0, 100), 'supported_features': 16}) await hass.async_block_till_done() state = hass.states.get('light.light_group') assert state.state == 'on' assert state.attributes['supported_features'] == 16 - assert state.attributes['rgb_color'] == (255, 0, 0) + assert state.attributes['hs_color'] == (0, 100) hass.states.async_set('light.test2', 'on', - {'rgb_color': (255, 255, 255), + {'hs_color': (0, 50), 'supported_features': 16}) await hass.async_block_till_done() state = hass.states.get('light.light_group') - assert state.attributes['rgb_color'] == (255, 127, 127) + assert state.attributes['hs_color'] == (0, 75) hass.states.async_set('light.test1', 'off', - {'rgb_color': (255, 0, 0), 'supported_features': 16}) + {'hs_color': (0, 0), 'supported_features': 16}) await hass.async_block_till_done() state = hass.states.get('light.light_group') - assert state.attributes['rgb_color'] == (255, 255, 255) + assert state.attributes['hs_color'] == (0, 50) async def test_white_value(hass): @@ -413,5 +383,7 @@ async def test_invalid_service_calls(hass): } await grouped_light.async_turn_on(**data) data['entity_id'] = ['light.test1', 'light.test2'] + data.pop('rgb_color') + data.pop('xy_color') mock_call.assert_called_once_with('light', 'turn_on', data, blocking=True) diff --git a/tests/components/light/test_init.py b/tests/components/light/test_init.py index d35321b4479..4e8fad261bd 100644 --- a/tests/components/light/test_init.py +++ b/tests/components/light/test_init.py @@ -188,23 +188,25 @@ class TestLight(unittest.TestCase): self.hass.block_till_done() _, data = dev1.last_call('turn_on') - self.assertEqual( - {light.ATTR_TRANSITION: 10, - light.ATTR_BRIGHTNESS: 20, - light.ATTR_RGB_COLOR: (0, 0, 255)}, - data) + self.assertEqual({ + light.ATTR_TRANSITION: 10, + light.ATTR_BRIGHTNESS: 20, + light.ATTR_HS_COLOR: (240, 100), + }, data) _, data = dev2.last_call('turn_on') - self.assertEqual( - {light.ATTR_RGB_COLOR: (255, 255, 255), - light.ATTR_WHITE_VALUE: 255}, - data) + self.assertEqual({ + light.ATTR_HS_COLOR: (0, 0), + light.ATTR_WHITE_VALUE: 255, + }, data) _, data = dev3.last_call('turn_on') - self.assertEqual({light.ATTR_XY_COLOR: (.4, .6)}, data) + self.assertEqual({ + light.ATTR_HS_COLOR: (71.059, 100), + }, data) # One of the light profiles - prof_name, prof_x, prof_y, prof_bri = 'relax', 0.5119, 0.4147, 144 + prof_name, prof_h, prof_s, prof_bri = 'relax', 35.932, 69.412, 144 # Test light profiles light.turn_on(self.hass, dev1.entity_id, profile=prof_name) @@ -216,16 +218,16 @@ class TestLight(unittest.TestCase): self.hass.block_till_done() _, data = dev1.last_call('turn_on') - self.assertEqual( - {light.ATTR_BRIGHTNESS: prof_bri, - light.ATTR_XY_COLOR: (prof_x, prof_y)}, - data) + self.assertEqual({ + light.ATTR_BRIGHTNESS: prof_bri, + light.ATTR_HS_COLOR: (prof_h, prof_s), + }, data) _, data = dev2.last_call('turn_on') - self.assertEqual( - {light.ATTR_BRIGHTNESS: 100, - light.ATTR_XY_COLOR: (.5119, .4147)}, - data) + self.assertEqual({ + light.ATTR_BRIGHTNESS: 100, + light.ATTR_HS_COLOR: (prof_h, prof_s), + }, data) # Test bad data light.turn_on(self.hass) @@ -301,15 +303,16 @@ class TestLight(unittest.TestCase): _, data = dev1.last_call('turn_on') - self.assertEqual( - {light.ATTR_XY_COLOR: (.4, .6), light.ATTR_BRIGHTNESS: 100}, - data) + self.assertEqual({ + light.ATTR_HS_COLOR: (71.059, 100), + light.ATTR_BRIGHTNESS: 100 + }, data) async def test_intent_set_color(hass): """Test the set color intent.""" hass.states.async_set('light.hello_2', 'off', { - ATTR_SUPPORTED_FEATURES: light.SUPPORT_RGB_COLOR + ATTR_SUPPORTED_FEATURES: light.SUPPORT_COLOR }) hass.states.async_set('switch.hello', 'off') calls = async_mock_service(hass, light.DOMAIN, light.SERVICE_TURN_ON) @@ -364,7 +367,7 @@ async def test_intent_set_color_and_brightness(hass): """Test the set color intent.""" hass.states.async_set('light.hello_2', 'off', { ATTR_SUPPORTED_FEATURES: ( - light.SUPPORT_RGB_COLOR | light.SUPPORT_BRIGHTNESS) + light.SUPPORT_COLOR | light.SUPPORT_BRIGHTNESS) }) hass.states.async_set('switch.hello', 'off') calls = async_mock_service(hass, light.DOMAIN, light.SERVICE_TURN_ON) diff --git a/tests/components/light/test_mqtt.py b/tests/components/light/test_mqtt.py index 6c56564df69..71fe77ef6be 100644 --- a/tests/components/light/test_mqtt.py +++ b/tests/components/light/test_mqtt.py @@ -250,12 +250,12 @@ class TestLightMQTT(unittest.TestCase): state = self.hass.states.get('light.test') self.assertEqual(STATE_ON, state.state) - self.assertEqual([255, 255, 255], state.attributes.get('rgb_color')) + self.assertEqual((255, 255, 255), state.attributes.get('rgb_color')) self.assertEqual(255, state.attributes.get('brightness')) self.assertEqual(150, state.attributes.get('color_temp')) self.assertEqual('none', state.attributes.get('effect')) self.assertEqual(255, state.attributes.get('white_value')) - self.assertEqual([1, 1], state.attributes.get('xy_color')) + self.assertEqual((0.32, 0.336), state.attributes.get('xy_color')) fire_mqtt_message(self.hass, 'test_light_rgb/status', '0') self.hass.block_till_done() @@ -303,7 +303,7 @@ class TestLightMQTT(unittest.TestCase): self.hass.block_till_done() light_state = self.hass.states.get('light.test') - self.assertEqual([125, 125, 125], + self.assertEqual((255, 255, 255), light_state.attributes.get('rgb_color')) fire_mqtt_message(self.hass, 'test_light_rgb/xy/status', @@ -311,7 +311,7 @@ class TestLightMQTT(unittest.TestCase): self.hass.block_till_done() light_state = self.hass.states.get('light.test') - self.assertEqual([0.675, 0.322], + self.assertEqual((0.652, 0.343), light_state.attributes.get('xy_color')) def test_brightness_controlling_scale(self): @@ -458,11 +458,11 @@ class TestLightMQTT(unittest.TestCase): state = self.hass.states.get('light.test') self.assertEqual(STATE_ON, state.state) self.assertEqual(50, state.attributes.get('brightness')) - self.assertEqual([1, 2, 3], state.attributes.get('rgb_color')) + self.assertEqual((0, 123, 255), state.attributes.get('rgb_color')) self.assertEqual(300, state.attributes.get('color_temp')) self.assertEqual('rainbow', state.attributes.get('effect')) self.assertEqual(75, state.attributes.get('white_value')) - self.assertEqual([0.123, 0.123], state.attributes.get('xy_color')) + self.assertEqual((0.14, 0.131), state.attributes.get('xy_color')) def test_sending_mqtt_commands_and_optimistic(self): \ # pylint: disable=invalid-name @@ -516,18 +516,18 @@ class TestLightMQTT(unittest.TestCase): self.mock_publish.async_publish.assert_has_calls([ mock.call('test_light_rgb/set', 'on', 2, False), - mock.call('test_light_rgb/rgb/set', '75,75,75', 2, False), + mock.call('test_light_rgb/rgb/set', '50,50,50', 2, False), mock.call('test_light_rgb/brightness/set', 50, 2, False), mock.call('test_light_rgb/white_value/set', 80, 2, False), - mock.call('test_light_rgb/xy/set', '0.123,0.123', 2, False), + mock.call('test_light_rgb/xy/set', '0.32,0.336', 2, False), ], any_order=True) state = self.hass.states.get('light.test') self.assertEqual(STATE_ON, state.state) - self.assertEqual((75, 75, 75), state.attributes['rgb_color']) + self.assertEqual((255, 255, 255), state.attributes['rgb_color']) self.assertEqual(50, state.attributes['brightness']) self.assertEqual(80, state.attributes['white_value']) - self.assertEqual((0.123, 0.123), state.attributes['xy_color']) + self.assertEqual((0.32, 0.336), state.attributes['xy_color']) def test_sending_mqtt_rgb_command_with_template(self): """Test the sending of RGB command with template.""" @@ -554,12 +554,12 @@ class TestLightMQTT(unittest.TestCase): self.mock_publish.async_publish.assert_has_calls([ mock.call('test_light_rgb/set', 'on', 0, False), - mock.call('test_light_rgb/rgb/set', '#ff8040', 0, False), + mock.call('test_light_rgb/rgb/set', '#ff803f', 0, False), ], any_order=True) state = self.hass.states.get('light.test') self.assertEqual(STATE_ON, state.state) - self.assertEqual((255, 128, 64), state.attributes['rgb_color']) + self.assertEqual((255, 128, 63), state.attributes['rgb_color']) def test_show_brightness_if_only_command_topic(self): """Test the brightness if only a command topic is present.""" @@ -679,7 +679,7 @@ class TestLightMQTT(unittest.TestCase): state = self.hass.states.get('light.test') self.assertEqual(STATE_ON, state.state) - self.assertEqual([1, 1], state.attributes.get('xy_color')) + self.assertEqual((0.32, 0.336), state.attributes.get('xy_color')) def test_on_command_first(self): """Test on command being sent before brightness.""" @@ -799,7 +799,7 @@ class TestLightMQTT(unittest.TestCase): self.hass.block_till_done() self.mock_publish.async_publish.assert_has_calls([ - mock.call('test_light/rgb', '75,75,75', 0, False), + mock.call('test_light/rgb', '50,50,50', 0, False), mock.call('test_light/bright', 50, 0, False) ], any_order=True) diff --git a/tests/components/light/test_mqtt_json.py b/tests/components/light/test_mqtt_json.py index ba306a81a34..cfeffc93108 100644 --- a/tests/components/light/test_mqtt_json.py +++ b/tests/components/light/test_mqtt_json.py @@ -180,7 +180,7 @@ class TestLightMQTTJSON(unittest.TestCase): state = self.hass.states.get('light.test') self.assertEqual(STATE_OFF, state.state) - self.assertEqual(255, state.attributes.get(ATTR_SUPPORTED_FEATURES)) + self.assertEqual(191, state.attributes.get(ATTR_SUPPORTED_FEATURES)) self.assertIsNone(state.attributes.get('rgb_color')) self.assertIsNone(state.attributes.get('brightness')) self.assertIsNone(state.attributes.get('color_temp')) @@ -192,8 +192,7 @@ class TestLightMQTTJSON(unittest.TestCase): # Turn on the light, full white fire_mqtt_message(self.hass, 'test_light_rgb', '{"state":"ON",' - '"color":{"r":255,"g":255,"b":255,' - '"x":0.123,"y":0.123},' + '"color":{"r":255,"g":255,"b":255},' '"brightness":255,' '"color_temp":155,' '"effect":"colorloop",' @@ -202,12 +201,12 @@ class TestLightMQTTJSON(unittest.TestCase): state = self.hass.states.get('light.test') self.assertEqual(STATE_ON, state.state) - self.assertEqual([255, 255, 255], state.attributes.get('rgb_color')) + self.assertEqual((255, 255, 255), state.attributes.get('rgb_color')) self.assertEqual(255, state.attributes.get('brightness')) self.assertEqual(155, state.attributes.get('color_temp')) self.assertEqual('colorloop', state.attributes.get('effect')) self.assertEqual(150, state.attributes.get('white_value')) - self.assertEqual([0.123, 0.123], state.attributes.get('xy_color')) + self.assertEqual((0.32, 0.336), state.attributes.get('xy_color')) # Turn the light off fire_mqtt_message(self.hass, 'test_light_rgb', '{"state":"OFF"}') @@ -232,7 +231,7 @@ class TestLightMQTTJSON(unittest.TestCase): self.hass.block_till_done() light_state = self.hass.states.get('light.test') - self.assertEqual([125, 125, 125], + self.assertEqual((255, 255, 255), light_state.attributes.get('rgb_color')) fire_mqtt_message(self.hass, 'test_light_rgb', @@ -241,7 +240,7 @@ class TestLightMQTTJSON(unittest.TestCase): self.hass.block_till_done() light_state = self.hass.states.get('light.test') - self.assertEqual([0.135, 0.135], + self.assertEqual((0.141, 0.14), light_state.attributes.get('xy_color')) fire_mqtt_message(self.hass, 'test_light_rgb', @@ -503,7 +502,7 @@ class TestLightMQTTJSON(unittest.TestCase): state = self.hass.states.get('light.test') self.assertEqual(STATE_ON, state.state) - self.assertEqual([255, 255, 255], state.attributes.get('rgb_color')) + self.assertEqual((255, 255, 255), state.attributes.get('rgb_color')) self.assertEqual(255, state.attributes.get('brightness')) self.assertEqual(255, state.attributes.get('white_value')) @@ -516,7 +515,7 @@ class TestLightMQTTJSON(unittest.TestCase): # Color should not have changed state = self.hass.states.get('light.test') self.assertEqual(STATE_ON, state.state) - self.assertEqual([255, 255, 255], state.attributes.get('rgb_color')) + self.assertEqual((255, 255, 255), state.attributes.get('rgb_color')) # Bad brightness values fire_mqtt_message(self.hass, 'test_light_rgb', diff --git a/tests/components/light/test_mqtt_template.py b/tests/components/light/test_mqtt_template.py index 5a01aa15fa2..90d68dd10d2 100644 --- a/tests/components/light/test_mqtt_template.py +++ b/tests/components/light/test_mqtt_template.py @@ -151,7 +151,7 @@ class TestLightMQTTTemplate(unittest.TestCase): state = self.hass.states.get('light.test') self.assertEqual(STATE_ON, state.state) - self.assertEqual([255, 128, 64], state.attributes.get('rgb_color')) + self.assertEqual((255, 128, 63), state.attributes.get('rgb_color')) self.assertEqual(255, state.attributes.get('brightness')) self.assertEqual(145, state.attributes.get('color_temp')) self.assertEqual(123, state.attributes.get('white_value')) @@ -185,7 +185,8 @@ class TestLightMQTTTemplate(unittest.TestCase): self.hass.block_till_done() light_state = self.hass.states.get('light.test') - self.assertEqual([41, 42, 43], light_state.attributes.get('rgb_color')) + self.assertEqual((243, 249, 255), + light_state.attributes.get('rgb_color')) # change the white value fire_mqtt_message(self.hass, 'test_light_rgb', 'on,,,134') @@ -254,7 +255,7 @@ class TestLightMQTTTemplate(unittest.TestCase): self.hass.block_till_done() self.mock_publish.async_publish.assert_called_once_with( - 'test_light_rgb/set', 'on,50,,,75-75-75', 2, False) + 'test_light_rgb/set', 'on,50,,,50-50-50', 2, False) self.mock_publish.async_publish.reset_mock() # turn on the light with color temp and white val @@ -267,7 +268,7 @@ class TestLightMQTTTemplate(unittest.TestCase): # check the state state = self.hass.states.get('light.test') self.assertEqual(STATE_ON, state.state) - self.assertEqual((75, 75, 75), state.attributes['rgb_color']) + self.assertEqual((255, 255, 255), state.attributes['rgb_color']) self.assertEqual(50, state.attributes['brightness']) self.assertEqual(200, state.attributes['color_temp']) self.assertEqual(139, state.attributes['white_value']) @@ -387,7 +388,7 @@ class TestLightMQTTTemplate(unittest.TestCase): self.assertEqual(STATE_ON, state.state) self.assertEqual(255, state.attributes.get('brightness')) self.assertEqual(215, state.attributes.get('color_temp')) - self.assertEqual([255, 255, 255], state.attributes.get('rgb_color')) + self.assertEqual((255, 255, 255), state.attributes.get('rgb_color')) self.assertEqual(222, state.attributes.get('white_value')) self.assertEqual('rainbow', state.attributes.get('effect')) @@ -421,7 +422,7 @@ class TestLightMQTTTemplate(unittest.TestCase): # color should not have changed state = self.hass.states.get('light.test') - self.assertEqual([255, 255, 255], state.attributes.get('rgb_color')) + self.assertEqual((255, 255, 255), state.attributes.get('rgb_color')) # bad white value values fire_mqtt_message(self.hass, 'test_light_rgb', 'on,,,off,255-255-255') diff --git a/tests/components/light/test_zwave.py b/tests/components/light/test_zwave.py index b925b74a7f0..4966b161360 100644 --- a/tests/components/light/test_zwave.py +++ b/tests/components/light/test_zwave.py @@ -4,9 +4,9 @@ from unittest.mock import patch, MagicMock import homeassistant.components.zwave from homeassistant.components.zwave import const from homeassistant.components.light import ( - zwave, ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_RGB_COLOR, ATTR_TRANSITION, - SUPPORT_BRIGHTNESS, SUPPORT_TRANSITION, SUPPORT_RGB_COLOR, - SUPPORT_COLOR_TEMP) + zwave, ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, ATTR_TRANSITION, + SUPPORT_BRIGHTNESS, SUPPORT_TRANSITION, SUPPORT_COLOR, ATTR_WHITE_VALUE, + SUPPORT_COLOR_TEMP, SUPPORT_WHITE_VALUE) from tests.mock.zwave import ( MockNode, MockValue, MockEntityValues, value_changed) @@ -42,7 +42,7 @@ def test_get_device_detects_colorlight(mock_openzwave): device = zwave.get_device(node=node, values=values, node_config={}) assert isinstance(device, zwave.ZwaveColorLight) - assert device.supported_features == SUPPORT_BRIGHTNESS | SUPPORT_RGB_COLOR + assert device.supported_features == SUPPORT_BRIGHTNESS | SUPPORT_COLOR def test_get_device_detects_zw098(mock_openzwave): @@ -54,7 +54,23 @@ def test_get_device_detects_zw098(mock_openzwave): device = zwave.get_device(node=node, values=values, node_config={}) assert isinstance(device, zwave.ZwaveColorLight) assert device.supported_features == ( - SUPPORT_BRIGHTNESS | SUPPORT_RGB_COLOR | SUPPORT_COLOR_TEMP) + SUPPORT_BRIGHTNESS | SUPPORT_COLOR | SUPPORT_COLOR_TEMP) + + +def test_get_device_detects_rgbw_light(mock_openzwave): + """Test get_device returns a color light.""" + node = MockNode(command_classes=[const.COMMAND_CLASS_SWITCH_COLOR]) + value = MockValue(data=0, node=node) + color = MockValue(data='#0000000000', node=node) + color_channels = MockValue(data=0x1d, node=node) + values = MockLightValues( + primary=value, color=color, color_channels=color_channels) + + device = zwave.get_device(node=node, values=values, node_config={}) + device.value_added() + assert isinstance(device, zwave.ZwaveColorLight) + assert device.supported_features == ( + SUPPORT_BRIGHTNESS | SUPPORT_COLOR | SUPPORT_WHITE_VALUE) def test_dimmer_turn_on(mock_openzwave): @@ -203,7 +219,7 @@ def test_dimmer_refresh_value(mock_openzwave): assert device.brightness == 118 -def test_set_rgb_color(mock_openzwave): +def test_set_hs_color(mock_openzwave): """Test setting zwave light color.""" node = MockNode(command_classes=[const.COMMAND_CLASS_SWITCH_COLOR]) value = MockValue(data=0, node=node) @@ -216,12 +232,12 @@ def test_set_rgb_color(mock_openzwave): assert color.data == '#0000000000' - device.turn_on(**{ATTR_RGB_COLOR: (200, 150, 100)}) + device.turn_on(**{ATTR_HS_COLOR: (30, 50)}) - assert color.data == '#c896640000' + assert color.data == '#ffbf7f0000' -def test_set_rgbw_color(mock_openzwave): +def test_set_white_value(mock_openzwave): """Test setting zwave light color.""" node = MockNode(command_classes=[const.COMMAND_CLASS_SWITCH_COLOR]) value = MockValue(data=0, node=node) @@ -234,9 +250,9 @@ def test_set_rgbw_color(mock_openzwave): assert color.data == '#0000000000' - device.turn_on(**{ATTR_RGB_COLOR: (200, 150, 100)}) + device.turn_on(**{ATTR_WHITE_VALUE: 200}) - assert color.data == '#c86400c800' + assert color.data == '#ffffffc800' def test_zw098_set_color_temp(mock_openzwave): @@ -273,7 +289,7 @@ def test_rgb_not_supported(mock_openzwave): color_channels=color_channels) device = zwave.get_device(node=node, values=values, node_config={}) - assert device.rgb_color is None + assert device.hs_color is None def test_no_color_value(mock_openzwave): @@ -283,7 +299,7 @@ def test_no_color_value(mock_openzwave): values = MockLightValues(primary=value) device = zwave.get_device(node=node, values=values, node_config={}) - assert device.rgb_color is None + assert device.hs_color is None def test_no_color_channels_value(mock_openzwave): @@ -294,7 +310,7 @@ def test_no_color_channels_value(mock_openzwave): values = MockLightValues(primary=value, color=color) device = zwave.get_device(node=node, values=values, node_config={}) - assert device.rgb_color is None + assert device.hs_color is None def test_rgb_value_changed(mock_openzwave): @@ -308,12 +324,12 @@ def test_rgb_value_changed(mock_openzwave): color_channels=color_channels) device = zwave.get_device(node=node, values=values, node_config={}) - assert device.rgb_color == [0, 0, 0] + assert device.hs_color == (0, 0) - color.data = '#c896640000' + color.data = '#ffbf800000' value_changed(color) - assert device.rgb_color == [200, 150, 100] + assert device.hs_color == (29.764, 49.804) def test_rgbww_value_changed(mock_openzwave): @@ -327,12 +343,14 @@ def test_rgbww_value_changed(mock_openzwave): color_channels=color_channels) device = zwave.get_device(node=node, values=values, node_config={}) - assert device.rgb_color == [0, 0, 0] + assert device.hs_color == (0, 0) + assert device.white_value == 0 color.data = '#c86400c800' value_changed(color) - assert device.rgb_color == [200, 150, 100] + assert device.hs_color == (30, 100) + assert device.white_value == 200 def test_rgbcw_value_changed(mock_openzwave): @@ -346,12 +364,14 @@ def test_rgbcw_value_changed(mock_openzwave): color_channels=color_channels) device = zwave.get_device(node=node, values=values, node_config={}) - assert device.rgb_color == [0, 0, 0] + assert device.hs_color == (0, 0) + assert device.white_value == 0 color.data = '#c86400c800' value_changed(color) - assert device.rgb_color == [200, 150, 100] + assert device.hs_color == (30, 100) + assert device.white_value == 200 def test_ct_value_changed(mock_openzwave): diff --git a/tests/testing_config/.remember_the_milk.conf b/tests/testing_config/.remember_the_milk.conf new file mode 100644 index 00000000000..272ac0903bd --- /dev/null +++ b/tests/testing_config/.remember_the_milk.conf @@ -0,0 +1 @@ +{"myprofile": {"id_map": {}}} \ No newline at end of file diff --git a/tests/testing_config/custom_components/light/test.py b/tests/testing_config/custom_components/light/test.py index fafe88eecbe..71625dfdf93 100644 --- a/tests/testing_config/custom_components/light/test.py +++ b/tests/testing_config/custom_components/light/test.py @@ -1,5 +1,5 @@ """ -Provide a mock switch platform. +Provide a mock light platform. Call init before using it in your tests to ensure clean test data. """ diff --git a/tests/util/test_color.py b/tests/util/test_color.py index 86d303c23b7..b64cf0acf80 100644 --- a/tests/util/test_color.py +++ b/tests/util/test_color.py @@ -10,26 +10,52 @@ class TestColorUtil(unittest.TestCase): """Test color util methods.""" # pylint: disable=invalid-name - def test_color_RGB_to_xy(self): - """Test color_RGB_to_xy.""" - self.assertEqual((0, 0, 0), color_util.color_RGB_to_xy(0, 0, 0)) + def test_color_RGB_to_xy_brightness(self): + """Test color_RGB_to_xy_brightness.""" + self.assertEqual((0, 0, 0), + color_util.color_RGB_to_xy_brightness(0, 0, 0)) self.assertEqual((0.32, 0.336, 255), - color_util.color_RGB_to_xy(255, 255, 255)) + color_util.color_RGB_to_xy_brightness(255, 255, 255)) self.assertEqual((0.136, 0.04, 12), - color_util.color_RGB_to_xy(0, 0, 255)) + color_util.color_RGB_to_xy_brightness(0, 0, 255)) self.assertEqual((0.172, 0.747, 170), - color_util.color_RGB_to_xy(0, 255, 0)) + color_util.color_RGB_to_xy_brightness(0, 255, 0)) self.assertEqual((0.679, 0.321, 80), + color_util.color_RGB_to_xy_brightness(255, 0, 0)) + + self.assertEqual((0.679, 0.321, 17), + color_util.color_RGB_to_xy_brightness(128, 0, 0)) + + def test_color_RGB_to_xy(self): + """Test color_RGB_to_xy.""" + self.assertEqual((0, 0), + color_util.color_RGB_to_xy(0, 0, 0)) + self.assertEqual((0.32, 0.336), + color_util.color_RGB_to_xy(255, 255, 255)) + + self.assertEqual((0.136, 0.04), + color_util.color_RGB_to_xy(0, 0, 255)) + + self.assertEqual((0.172, 0.747), + color_util.color_RGB_to_xy(0, 255, 0)) + + self.assertEqual((0.679, 0.321), color_util.color_RGB_to_xy(255, 0, 0)) + self.assertEqual((0.679, 0.321), + color_util.color_RGB_to_xy(128, 0, 0)) + def test_color_xy_brightness_to_RGB(self): - """Test color_RGB_to_xy.""" + """Test color_xy_brightness_to_RGB.""" self.assertEqual((0, 0, 0), color_util.color_xy_brightness_to_RGB(1, 1, 0)) + self.assertEqual((194, 186, 169), + color_util.color_xy_brightness_to_RGB(.35, .35, 128)) + self.assertEqual((255, 243, 222), color_util.color_xy_brightness_to_RGB(.35, .35, 255)) @@ -42,6 +68,20 @@ class TestColorUtil(unittest.TestCase): self.assertEqual((0, 63, 255), color_util.color_xy_brightness_to_RGB(0, 0, 255)) + def test_color_xy_to_RGB(self): + """Test color_xy_to_RGB.""" + self.assertEqual((255, 243, 222), + color_util.color_xy_to_RGB(.35, .35)) + + self.assertEqual((255, 0, 60), + color_util.color_xy_to_RGB(1, 0)) + + self.assertEqual((0, 255, 0), + color_util.color_xy_to_RGB(0, 1)) + + self.assertEqual((0, 63, 255), + color_util.color_xy_to_RGB(0, 0)) + def test_color_RGB_to_hsv(self): """Test color_RGB_to_hsv.""" self.assertEqual((0, 0, 0), @@ -110,6 +150,23 @@ class TestColorUtil(unittest.TestCase): self.assertEqual((225.176, 100), color_util.color_xy_to_hs(0, 0)) + def test_color_hs_to_xy(self): + """Test color_hs_to_xy.""" + self.assertEqual((0.151, 0.343), + color_util.color_hs_to_xy(180, 100)) + + self.assertEqual((0.352, 0.329), + color_util.color_hs_to_xy(350, 12.5)) + + self.assertEqual((0.228, 0.476), + color_util.color_hs_to_xy(140, 50)) + + self.assertEqual((0.465, 0.33), + color_util.color_hs_to_xy(0, 40)) + + self.assertEqual((0.32, 0.336), + color_util.color_hs_to_xy(360, 0)) + def test_rgb_hex_to_rgb_list(self): """Test rgb_hex_to_rgb_list.""" self.assertEqual([255, 255, 255], From 49683181d161fa0cf079a86ba493101a56873ae0 Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Mon, 19 Mar 2018 03:42:23 +0000 Subject: [PATCH 144/220] Superfluous None (#13326) --- homeassistant/components/sensor/filter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/filter.py b/homeassistant/components/sensor/filter.py index aad7fec26a0..3faf51a5f47 100644 --- a/homeassistant/components/sensor/filter.py +++ b/homeassistant/components/sensor/filter.py @@ -74,7 +74,7 @@ FILTER_TIME_SMA_SCHEMA = FILTER_SCHEMA.extend({ vol.Required(CONF_FILTER_NAME): FILTER_NAME_TIME_SMA, vol.Optional(CONF_TIME_SMA_TYPE, default=TIME_SMA_LAST): vol.In( - [None, TIME_SMA_LAST]), + [TIME_SMA_LAST]), vol.Required(CONF_FILTER_WINDOW_SIZE): vol.All(cv.time_period, cv.positive_timedelta) From 947218d51c94566feeb32c6f549493fa07683252 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Mon, 19 Mar 2018 10:47:10 +0100 Subject: [PATCH 145/220] pytest 3.4.0 cache gitignore (#13308) --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index b3774b06bc8..33a1f4f9a4b 100644 --- a/.gitignore +++ b/.gitignore @@ -21,7 +21,7 @@ Icon *.iml # pytest -.cache +.pytest_cache # GITHUB Proposed Python stuff: *.py[cod] From 2bc7e587808043be82514077ac3215e10f5b6d8b Mon Sep 17 00:00:00 2001 From: Qxlkdr <33372537+Qxlkdr@users.noreply.github.com> Date: Mon, 19 Mar 2018 21:38:07 +0100 Subject: [PATCH 146/220] Add trafikverket_weatherstation sensor platform (#12115) * Create trafikverket_weatherstation.py Created PR 12111 but due to permission issue I'm creating a new fork and PR. * Added dot Added dot to the first (second) row of the file, after the description. * Corrections based on feedback Done: - Run flake8 before this commit - Fixed invalid variables - Shortened the xml variable/query via if statement (air_vs_road) - Moved imports if update() to top of the file - Imported CONF_API_KEY and CONF_TYPE - Updated documentation (api_key): home-assistant/home-assistant.github.io#4562 Actions left: - Error handling - Request timeout - Add sensor (file) to .coveragerc * Multiple corrections Done: - Executed pylint and flake8 tests before commit - Fixed import order - Implemented request timeout - Used variable air_vs_road in the return as well Actions left: - Error handling - Add sensor (file) to .coveragerc * Error handling Done: - Error handling for network - Error handling for JSON response * Added trafikverket_weatherstation.py Added trafikverket_weatherstation.py in correct order. * Road as default Changed if statement to check for 'road' which means it will always defaulting to 'air' in other cases. Even if it will only accept 'air' and 'road' from the PLATFORM_SCHEMA. * Updated variable names Updated variable names to be more understandable as requested by @MartinHjelmare * Standard Libraries Grouped Standard Libraries * Return None Changed return None to only return as suggested. --- .coveragerc | 1 + .../sensor/trafikverket_weatherstation.py | 124 ++++++++++++++++++ 2 files changed, 125 insertions(+) create mode 100644 homeassistant/components/sensor/trafikverket_weatherstation.py diff --git a/.coveragerc b/.coveragerc index 40fccd5e921..1dcde0ded14 100644 --- a/.coveragerc +++ b/.coveragerc @@ -665,6 +665,7 @@ omit = homeassistant/components/sensor/tibber.py homeassistant/components/sensor/time_date.py homeassistant/components/sensor/torque.py + homeassistant/components/sensor/trafikverket_weatherstation.py homeassistant/components/sensor/transmission.py homeassistant/components/sensor/travisci.py homeassistant/components/sensor/twitch.py diff --git a/homeassistant/components/sensor/trafikverket_weatherstation.py b/homeassistant/components/sensor/trafikverket_weatherstation.py new file mode 100644 index 00000000000..fba16c27c7e --- /dev/null +++ b/homeassistant/components/sensor/trafikverket_weatherstation.py @@ -0,0 +1,124 @@ +""" +Weather information for air and road temperature, provided by Trafikverket. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.trafikverket_weatherstation/ +""" +import json +import logging +from datetime import timedelta + +import requests +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import ( + CONF_NAME, ATTR_ATTRIBUTION, TEMP_CELSIUS, CONF_API_KEY, CONF_TYPE) +from homeassistant.helpers.entity import Entity +from homeassistant.util import Throttle + +_LOGGER = logging.getLogger(__name__) + +CONF_ATTRIBUTION = "Data provided by Trafikverket API" + +CONF_STATION = 'station' + +MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) +SCAN_INTERVAL = timedelta(seconds=300) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_NAME): cv.string, + vol.Required(CONF_API_KEY): cv.string, + vol.Required(CONF_STATION): cv.string, + vol.Required(CONF_TYPE): vol.In(['air', 'road']), +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the sensor platform.""" + sensor_name = config.get(CONF_NAME) + sensor_api = config.get(CONF_API_KEY) + sensor_station = config.get(CONF_STATION) + sensor_type = config.get(CONF_TYPE) + + add_devices([TrafikverketWeatherStation( + sensor_name, sensor_api, sensor_station, sensor_type)], True) + + +class TrafikverketWeatherStation(Entity): + """Representation of a Sensor.""" + + def __init__(self, sensor_name, sensor_api, sensor_station, sensor_type): + """Initialize the sensor.""" + self._name = sensor_name + self._api = sensor_api + self._station = sensor_station + self._type = sensor_type + self._state = None + self._attributes = { + ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + } + + @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.""" + return TEMP_CELSIUS + + @property + def device_state_attributes(self): + """Return the state attributes.""" + return self._attributes + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + def update(self): + """Fetch new state data for the sensor. + + This is the only method that should fetch new data for Home Assistant. + """ + url = 'http://api.trafikinfo.trafikverket.se/v1.3/data.json' + + if self._type == 'road': + air_vs_road = 'Road' + else: + air_vs_road = 'Air' + + xml = """ + + + + + + + Measurement.""" + air_vs_road + """.Temp + + """ + + # Testing JSON post request. + try: + post = requests.post(url, data=xml.encode('utf-8'), timeout=5) + except requests.exceptions.RequestException as err: + _LOGGER.error("Please check network connection: %s", err) + return + + # Checking JSON respons. + try: + data = json.loads(post.text) + result = data["RESPONSE"]["RESULT"][0] + final = result["WeatherStation"][0]["Measurement"] + except KeyError: + _LOGGER.error("Incorrect weather station or API key.") + return + + # air_vs_road contains "Air" or "Road" depending on user input. + self._state = final[air_vs_road]["Temp"] From f2879554228a036d47d5477984179754f01a5a6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Per=20Osb=C3=A4ck?= Date: Mon, 19 Mar 2018 22:12:53 +0100 Subject: [PATCH 147/220] zha: catch the exception from bellows if a device isn't available. (#13314) * catch the exception from bellows if a device isn't available. * fix import of zigpy exception * fix lint import --- homeassistant/components/light/zha.py | 16 +++++++++++++--- homeassistant/components/switch/zha.py | 16 ++++++++++++++-- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/light/zha.py b/homeassistant/components/light/zha.py index 68c5bcc2a29..8eb1b3dc9b6 100644 --- a/homeassistant/components/light/zha.py +++ b/homeassistant/components/light/zha.py @@ -5,7 +5,6 @@ For more details on this platform, please refer to the documentation at https://home-assistant.io/components/light.zha/ """ import logging - from homeassistant.components import light, zha from homeassistant.const import STATE_UNKNOWN import homeassistant.util.color as color_util @@ -112,14 +111,25 @@ class Light(zha.Entity, light.Light): self._state = 1 self.async_schedule_update_ha_state() return + from zigpy.exceptions import DeliveryError + try: + await self._endpoint.on_off.on() + except DeliveryError as ex: + _LOGGER.error("Unable to turn the light on: %s", ex) + return - await self._endpoint.on_off.on() self._state = 1 self.async_schedule_update_ha_state() async def async_turn_off(self, **kwargs): """Turn the entity off.""" - await self._endpoint.on_off.off() + from zigpy.exceptions import DeliveryError + try: + await self._endpoint.on_off.off() + except DeliveryError as ex: + _LOGGER.error("Unable to turn the light off: %s", ex) + return + self._state = 0 self.async_schedule_update_ha_state() diff --git a/homeassistant/components/switch/zha.py b/homeassistant/components/switch/zha.py index 7de9f1459b1..22eb50be86b 100644 --- a/homeassistant/components/switch/zha.py +++ b/homeassistant/components/switch/zha.py @@ -57,12 +57,24 @@ class Switch(zha.Entity, SwitchDevice): async def async_turn_on(self, **kwargs): """Turn the entity on.""" - await self._endpoint.on_off.on() + from zigpy.exceptions import DeliveryError + try: + await self._endpoint.on_off.on() + except DeliveryError as ex: + _LOGGER.error("Unable to turn the switch on: %s", ex) + return + self._state = 1 async def async_turn_off(self, **kwargs): """Turn the entity off.""" - await self._endpoint.on_off.off() + from zigpy.exceptions import DeliveryError + try: + await self._endpoint.on_off.off() + except DeliveryError as ex: + _LOGGER.error("Unable to turn the switch off: %s", ex) + return + self._state = 0 async def async_update(self): From a04c6d583038b6303ebaaee4e6c96bcc1dffc443 Mon Sep 17 00:00:00 2001 From: Ryan McLean Date: Mon, 19 Mar 2018 21:15:21 +0000 Subject: [PATCH 148/220] Plex unavailable client cleanup (#13156) * Added timestamp for when device was marked unavailable * protect time 1st marked * client removal, no errors * Optional removal interval added * Linting error fix * Implemented guard to prevent indentation Removed vars in favour of inline calcs * Using hass.add_job() for cleanup * Lint * Revert removal interval to 600sec * Changed datetime to hass implementation * Forgot to include one of the references to dt --- homeassistant/components/media_player/plex.py | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/homeassistant/components/media_player/plex.py b/homeassistant/components/media_player/plex.py index 48e532074f7..edb8aa147fb 100644 --- a/homeassistant/components/media_player/plex.py +++ b/homeassistant/components/media_player/plex.py @@ -23,6 +23,8 @@ from homeassistant.const import ( from homeassistant.helpers import config_validation as cv from homeassistant.helpers.event import track_utc_time_change from homeassistant.util.json import load_json, save_json +from homeassistant.util import dt as dt_util + REQUIREMENTS = ['plexapi==3.0.6'] @@ -38,6 +40,8 @@ CONF_INCLUDE_NON_CLIENTS = 'include_non_clients' CONF_USE_EPISODE_ART = 'use_episode_art' CONF_USE_CUSTOM_ENTITY_IDS = 'use_custom_entity_ids' CONF_SHOW_ALL_CONTROLS = 'show_all_controls' +CONF_REMOVE_UNAVAILABLE_CLIENTS = 'remove_unavailable_clients' +CONF_CLIENT_REMOVE_INTERVAL = 'client_remove_interval' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_INCLUDE_NON_CLIENTS, default=False): @@ -46,6 +50,10 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ cv.boolean, vol.Optional(CONF_USE_CUSTOM_ENTITY_IDS, default=False): cv.boolean, + vol.Optional(CONF_REMOVE_UNAVAILABLE_CLIENTS, default=True): + cv.boolean, + vol.Optional(CONF_CLIENT_REMOVE_INTERVAL, default=timedelta(seconds=600)): + vol.All(cv.time_period, cv.positive_timedelta), }) PLEX_DATA = "plex" @@ -184,6 +192,7 @@ def setup_plexserver( else: plex_clients[machine_identifier].refresh(None, session) + clients_to_remove = [] for client in plex_clients.values(): # force devices to idle that do not have a valid session if client.session is None: @@ -192,6 +201,18 @@ def setup_plexserver( client.set_availability(client.machine_identifier in available_client_ids) + if not config.get(CONF_REMOVE_UNAVAILABLE_CLIENTS) \ + or client.available: + continue + + if (dt_util.utcnow() - client.marked_unavailable) >= \ + (config.get(CONF_CLIENT_REMOVE_INTERVAL)): + hass.add_job(client.async_remove()) + clients_to_remove.append(client.machine_identifier) + + while clients_to_remove: + del plex_clients[clients_to_remove.pop()] + if new_plex_clients: add_devices_callback(new_plex_clients) @@ -266,6 +287,7 @@ class PlexClient(MediaPlayerDevice): self._app_name = '' self._device = None self._available = False + self._marked_unavailable = None self._device_protocol_capabilities = None self._is_player_active = False self._is_player_available = False @@ -418,6 +440,11 @@ class PlexClient(MediaPlayerDevice): """Set the device as available/unavailable noting time.""" if not available: self._clear_media_details() + if self._marked_unavailable is None: + self._marked_unavailable = dt_util.utcnow() + else: + self._marked_unavailable = None + self._available = available def _set_player_state(self): @@ -506,6 +533,11 @@ class PlexClient(MediaPlayerDevice): """Return the device, if any.""" return self._device + @property + def marked_unavailable(self): + """Return time device was marked unavailable.""" + return self._marked_unavailable + @property def session(self): """Return the session, if any.""" From 4270bc7abb7a2fe6aa4f8fbca8908c03338dd3c2 Mon Sep 17 00:00:00 2001 From: Johann Kellerman Date: Mon, 19 Mar 2018 23:20:04 +0200 Subject: [PATCH 149/220] Perform check_config service in current process (#13017) * Perform check_config service in current process * feedback --- homeassistant/config.py | 22 ++++++---------------- tests/test_config.py | 34 ++++++++++++---------------------- 2 files changed, 18 insertions(+), 38 deletions(-) diff --git a/homeassistant/config.py b/homeassistant/config.py index e94fc297f48..58cfe845e8f 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -1,5 +1,4 @@ """Module to help with parsing and generating configuration files.""" -import asyncio from collections import OrderedDict # pylint: disable=no-name-in-module from distutils.version import LooseVersion # pylint: disable=import-error @@ -7,7 +6,6 @@ import logging import os import re import shutil -import sys # pylint: disable=unused-import from typing import Any, List, Tuple # NOQA @@ -665,22 +663,14 @@ async def async_check_ha_config_file(hass): This method is a coroutine. """ - proc = await asyncio.create_subprocess_exec( - sys.executable, '-m', 'homeassistant', '--script', - 'check_config', '--config', hass.config.config_dir, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.STDOUT, loop=hass.loop) + from homeassistant.scripts.check_config import check_ha_config_file - # Wait for the subprocess exit - log, _ = await proc.communicate() - exit_code = await proc.wait() + res = await hass.async_add_job( + check_ha_config_file, hass.config.config_dir) - # Convert to ASCII - log = RE_ASCII.sub('', log.decode()) - - if exit_code != 0 or RE_YAML_ERROR.search(log): - return log - return None + if not res.errors: + return None + return '\n'.join([err.message for err in res.errors]) @callback diff --git a/tests/test_config.py b/tests/test_config.py index ab6b860ea8f..aaa793f91a9 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -27,9 +27,9 @@ from homeassistant.components.config.script import ( CONFIG_PATH as SCRIPTS_CONFIG_PATH) from homeassistant.components.config.customize import ( CONFIG_PATH as CUSTOMIZE_CONFIG_PATH) +import homeassistant.scripts.check_config as check_config -from tests.common import ( - get_test_config_dir, get_test_home_assistant, mock_coro) +from tests.common import get_test_config_dir, get_test_home_assistant CONFIG_DIR = get_test_config_dir() YAML_PATH = os.path.join(CONFIG_DIR, config_util.YAML_CONFIG_FILE) @@ -514,35 +514,25 @@ class TestConfig(unittest.TestCase): assert len(self.hass.config.whitelist_external_dirs) == 1 assert "/test/config/www" in self.hass.config.whitelist_external_dirs - @mock.patch('asyncio.create_subprocess_exec') - def test_check_ha_config_file_correct(self, mock_create): + @mock.patch('homeassistant.scripts.check_config.check_ha_config_file') + def test_check_ha_config_file_correct(self, mock_check): """Check that restart propagates to stop.""" - process_mock = mock.MagicMock() - attrs = { - 'communicate.return_value': mock_coro((b'output', None)), - 'wait.return_value': mock_coro(0)} - process_mock.configure_mock(**attrs) - mock_create.return_value = mock_coro(process_mock) - + mock_check.return_value = check_config.HomeAssistantConfig() assert run_coroutine_threadsafe( - config_util.async_check_ha_config_file(self.hass), self.hass.loop + config_util.async_check_ha_config_file(self.hass), + self.hass.loop ).result() is None - @mock.patch('asyncio.create_subprocess_exec') - def test_check_ha_config_file_wrong(self, mock_create): + @mock.patch('homeassistant.scripts.check_config.check_ha_config_file') + def test_check_ha_config_file_wrong(self, mock_check): """Check that restart with a bad config doesn't propagate to stop.""" - process_mock = mock.MagicMock() - attrs = { - 'communicate.return_value': - mock_coro(('\033[34mhello'.encode('utf-8'), None)), - 'wait.return_value': mock_coro(1)} - process_mock.configure_mock(**attrs) - mock_create.return_value = mock_coro(process_mock) + mock_check.return_value = check_config.HomeAssistantConfig() + mock_check.return_value.add_error("bad") assert run_coroutine_threadsafe( config_util.async_check_ha_config_file(self.hass), self.hass.loop - ).result() == 'hello' + ).result() == 'bad' # pylint: disable=redefined-outer-name From 0977be1842be1358c8687fd213584da6ad2c53a5 Mon Sep 17 00:00:00 2001 From: Sergio Viudes Date: Tue, 20 Mar 2018 08:43:31 +0100 Subject: [PATCH 150/220] Added switch for DoorBird second relay (#13339) --- homeassistant/components/doorbird.py | 2 +- homeassistant/components/switch/doorbird.py | 10 ++++++++++ requirements_all.txt | 2 +- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/doorbird.py b/homeassistant/components/doorbird.py index be7adc034a0..34758023f60 100644 --- a/homeassistant/components/doorbird.py +++ b/homeassistant/components/doorbird.py @@ -13,7 +13,7 @@ from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD from homeassistant.components.http import HomeAssistantView import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['DoorBirdPy==0.1.2'] +REQUIREMENTS = ['DoorBirdPy==0.1.3'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/switch/doorbird.py b/homeassistant/components/switch/doorbird.py index 4ab8eea6ec4..9886b3a586d 100644 --- a/homeassistant/components/switch/doorbird.py +++ b/homeassistant/components/switch/doorbird.py @@ -22,6 +22,14 @@ SWITCHES = { }, "time": datetime.timedelta(seconds=3) }, + "open_door_2": { + "name": "Open Door 2", + "icon": { + True: "lock-open", + False: "lock" + }, + "time": datetime.timedelta(seconds=3) + }, "light_on": { "name": "Light On", "icon": { @@ -80,6 +88,8 @@ class DoorBirdSwitch(SwitchDevice): """Power the relay.""" if self._switch == "open_door": self._state = self._device.open_door() + elif self._switch == "open_door_2": + self._state = self._device.open_door(2) elif self._switch == "light_on": self._state = self._device.turn_light_on() diff --git a/requirements_all.txt b/requirements_all.txt index 91173a1825c..e2696bdd934 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -19,7 +19,7 @@ attrs==17.4.0 # Adafruit_BBIO==1.0.0 # homeassistant.components.doorbird -DoorBirdPy==0.1.2 +DoorBirdPy==0.1.3 # homeassistant.components.homekit HAP-python==1.1.7 From 3fa080a795064c7181b007b107257b977d53b5f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Tue, 20 Mar 2018 08:46:10 +0100 Subject: [PATCH 151/220] Add min and max price as attribute for Tibber sensor (#13313) --- homeassistant/components/sensor/tibber.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sensor/tibber.py b/homeassistant/components/sensor/tibber.py index 435003f76d0..aaaa8366909 100644 --- a/homeassistant/components/sensor/tibber.py +++ b/homeassistant/components/sensor/tibber.py @@ -73,14 +73,25 @@ class TibberSensor(Entity): return def _find_current_price(): + state = None + max_price = None + min_price = None for key, price_total in self._tibber_home.price_total.items(): price_time = dt_util.as_utc(dt_util.parse_datetime(key)) + price_total = round(price_total, 3) time_diff = (now - price_time).total_seconds()/60 if time_diff >= 0 and time_diff < 60: - self._state = round(price_total, 3) + state = price_total self._last_updated = key - return True - return False + if now.date() == price_time.date(): + if max_price is None or price_total > max_price: + max_price = price_total + if min_price is None or price_total < min_price: + min_price = price_total + self._state = state + self._device_state_attributes['max_price'] = max_price + self._device_state_attributes['min_price'] = min_price + return state is not None if _find_current_price(): return From 5c4529d044463083bad73cdbf9d17d8cb2b29afa Mon Sep 17 00:00:00 2001 From: John Arild Berentsen Date: Tue, 20 Mar 2018 14:04:24 +0100 Subject: [PATCH 152/220] Bugfix: Zwave set_config_parameter failed when config list contained int (#13301) * Cast list and bool options to STR * Add button options to STR * Add TYPE_BUTTON to value types * Adjust comparison * Remove Logging * Remove Empty line * Update tests * Update tests * Mistake --- homeassistant/components/zwave/__init__.py | 13 +++++++-- homeassistant/components/zwave/const.py | 1 + tests/components/zwave/test_init.py | 34 +++++++++++++++++++++- 3 files changed, 44 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/zwave/__init__.py b/homeassistant/components/zwave/__init__.py index 43aa996c799..a85160e8bde 100644 --- a/homeassistant/components/zwave/__init__.py +++ b/homeassistant/components/zwave/__init__.py @@ -442,9 +442,16 @@ def setup(hass, config): if value.index != param: continue if value.type in [const.TYPE_LIST, const.TYPE_BOOL]: - value.data = selection - _LOGGER.info("Setting config list parameter %s on Node %s " - "with selection %s", param, node_id, + value.data = str(selection) + _LOGGER.info("Setting config parameter %s on Node %s " + "with list/bool selection %s", param, node_id, + str(selection)) + return + if value.type == const.TYPE_BUTTON: + network.manager.pressButton(value.value_id) + network.manager.releaseButton(value.value_id) + _LOGGER.info("Setting config parameter %s on Node %s " + "with button selection %s", param, node_id, selection) return value.data = int(selection) diff --git a/homeassistant/components/zwave/const.py b/homeassistant/components/zwave/const.py index bb4b33300e5..8e1a22047c1 100644 --- a/homeassistant/components/zwave/const.py +++ b/homeassistant/components/zwave/const.py @@ -327,6 +327,7 @@ TYPE_DECIMAL = "Decimal" TYPE_INT = "Int" TYPE_LIST = "List" TYPE_STRING = "String" +TYPE_BUTTON = "Button" DISC_COMMAND_CLASS = "command_class" DISC_COMPONENT = "component" diff --git a/tests/components/zwave/test_init.py b/tests/components/zwave/test_init.py index 30c9d3ba489..bb073459b48 100644 --- a/tests/components/zwave/test_init.py +++ b/tests/components/zwave/test_init.py @@ -995,8 +995,21 @@ class TestZWaveServices(unittest.TestCase): type=const.TYPE_LIST, data_items=['item1', 'item2', 'item3'], ) + value_list_int = MockValue( + index=15, + command_class=const.COMMAND_CLASS_CONFIGURATION, + type=const.TYPE_LIST, + data_items=['1', '2', '3'], + ) + value_button = MockValue( + index=14, + command_class=const.COMMAND_CLASS_CONFIGURATION, + type=const.TYPE_BUTTON, + ) node = MockNode(node_id=14) - node.get_values.return_value = {12: value, 13: value_list} + node.get_values.return_value = {12: value, 13: value_list, + 14: value_button, + 15: value_list_int} self.zwave_network.nodes = {14: node} self.hass.services.call('zwave', 'set_config_parameter', { @@ -1008,6 +1021,15 @@ class TestZWaveServices(unittest.TestCase): assert value_list.data == 'item3' + self.hass.services.call('zwave', 'set_config_parameter', { + const.ATTR_NODE_ID: 14, + const.ATTR_CONFIG_PARAMETER: 15, + const.ATTR_CONFIG_VALUE: 3, + }) + self.hass.block_till_done() + + assert value_list_int.data == '3' + self.hass.services.call('zwave', 'set_config_parameter', { const.ATTR_NODE_ID: 14, const.ATTR_CONFIG_PARAMETER: 12, @@ -1017,6 +1039,16 @@ class TestZWaveServices(unittest.TestCase): assert value.data == 7 + self.hass.services.call('zwave', 'set_config_parameter', { + const.ATTR_NODE_ID: 14, + const.ATTR_CONFIG_PARAMETER: 14, + const.ATTR_CONFIG_VALUE: True, + }) + self.hass.block_till_done() + + assert self.zwave_network.manager.pressButton.called + assert self.zwave_network.manager.releaseButton.called + self.hass.services.call('zwave', 'set_config_parameter', { const.ATTR_NODE_ID: 14, const.ATTR_CONFIG_PARAMETER: 19, From 05c9c5750017b147617eff153c0b5fbe984d7e7f Mon Sep 17 00:00:00 2001 From: Daniel Perna Date: Tue, 20 Mar 2018 20:32:59 +0100 Subject: [PATCH 153/220] Update pyhomematic to 0.1.40 (#13354) * Update __init__.py * Update requirements_all.txt --- homeassistant/components/homematic/__init__.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/homematic/__init__.py b/homeassistant/components/homematic/__init__.py index b913b58864d..1accf038575 100644 --- a/homeassistant/components/homematic/__init__.py +++ b/homeassistant/components/homematic/__init__.py @@ -20,7 +20,7 @@ from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv from homeassistant.loader import bind_hass -REQUIREMENTS = ['pyhomematic==0.1.39'] +REQUIREMENTS = ['pyhomematic==0.1.40'] DOMAIN = 'homematic' _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index e2696bdd934..f3637a40f64 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -752,7 +752,7 @@ pyhik==0.1.8 pyhiveapi==0.2.11 # homeassistant.components.homematic -pyhomematic==0.1.39 +pyhomematic==0.1.40 # homeassistant.components.sensor.hydroquebec pyhydroquebec==2.1.0 From 852eef8046f9950900dd3bd86549a3a90185d36d Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Wed, 21 Mar 2018 02:05:03 +0100 Subject: [PATCH 154/220] Fix Sonos radio stations with ampersand (#13293) --- homeassistant/components/media_player/sonos.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/media_player/sonos.py b/homeassistant/components/media_player/sonos.py index 34f30b5c2f4..091046a6e7a 100644 --- a/homeassistant/components/media_player/sonos.py +++ b/homeassistant/components/media_player/sonos.py @@ -798,7 +798,9 @@ class SonosDevice(MediaPlayerDevice): src = fav.pop() uri = src.reference.get_uri() if _is_radio_uri(uri): - self.soco.play_uri(uri, title=source) + # SoCo 0.14 fails to XML escape the title parameter + from xml.sax.saxutils import escape + self.soco.play_uri(uri, title=escape(source)) else: self.soco.clear_queue() self.soco.add_to_queue(src.reference) From cfb0b00c0cf22dbb2a5dc9c832d4dab1fa58398d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 20 Mar 2018 18:09:34 -0700 Subject: [PATCH 155/220] Do not include unavailable entities in Google Assistant SYNC (#13358) --- .../components/google_assistant/smart_home.py | 9 +++++- homeassistant/components/light/demo.py | 3 +- .../google_assistant/test_smart_home.py | 28 +++++++++++++++++++ 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/google_assistant/smart_home.py b/homeassistant/components/google_assistant/smart_home.py index 834d40c367c..7e746d48bed 100644 --- a/homeassistant/components/google_assistant/smart_home.py +++ b/homeassistant/components/google_assistant/smart_home.py @@ -94,9 +94,16 @@ class _GoogleEntity: https://developers.google.com/actions/smarthome/create-app#actiondevicessync """ - traits = self.traits() state = self.state + # When a state is unavailable, the attributes that describe + # capabilities will be stripped. For example, a light entity will miss + # the min/max mireds. Therefore they will be excluded from a sync. + if state.state == STATE_UNAVAILABLE: + return None + + traits = self.traits() + # Found no supported traits for this entity if not traits: return None diff --git a/homeassistant/components/light/demo.py b/homeassistant/components/light/demo.py index 05aecd542e2..ba27cbd3ac5 100644 --- a/homeassistant/components/light/demo.py +++ b/homeassistant/components/light/demo.py @@ -52,6 +52,7 @@ class DemoLight(Light): self._white = white self._effect_list = effect_list self._effect = effect + self._available = True @property def should_poll(self) -> bool: @@ -73,7 +74,7 @@ class DemoLight(Light): """Return availability.""" # This demo light is always available, but well-behaving components # should implement this to inform Home Assistant accordingly. - return True + return self._available @property def brightness(self) -> int: diff --git a/tests/components/google_assistant/test_smart_home.py b/tests/components/google_assistant/test_smart_home.py index 6523c22fee1..d7684dc90e0 100644 --- a/tests/components/google_assistant/test_smart_home.py +++ b/tests/components/google_assistant/test_smart_home.py @@ -259,3 +259,31 @@ def test_serialize_input_boolean(): 'type': 'action.devices.types.SWITCH', 'willReportState': False, } + + +async def test_unavailable_state_doesnt_sync(hass): + """Test that an unavailable entity does not sync over.""" + light = DemoLight( + None, 'Demo Light', + state=False, + hs_color=(180, 75), + ) + light.hass = hass + light.entity_id = 'light.demo_light' + light._available = False + await light.async_update_ha_state() + + result = await sh.async_handle_message(hass, BASIC_CONFIG, { + "requestId": REQ_ID, + "inputs": [{ + "intent": "action.devices.SYNC" + }] + }) + + assert result == { + 'requestId': REQ_ID, + 'payload': { + 'agentUserId': 'test-agent', + 'devices': [] + } + } From 3426487277de4da44103eb3d3d1f2cf2332f782c Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Wed, 21 Mar 2018 02:26:56 +0100 Subject: [PATCH 156/220] Fix mysensors light turn on hs color (#13349) --- homeassistant/components/light/mysensors.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/light/mysensors.py b/homeassistant/components/light/mysensors.py index 26e20ff387d..d595fc4b184 100644 --- a/homeassistant/components/light/mysensors.py +++ b/homeassistant/components/light/mysensors.py @@ -107,7 +107,11 @@ class MySensorsLight(mysensors.MySensorsEntity, Light): rgb = color_util.color_hs_to_RGB(*self._hs) white = self._white hex_color = self._values.get(self.value_type) - new_rgb = color_util.color_hs_to_RGB(*kwargs.get(ATTR_HS_COLOR)) + hs_color = kwargs.get(ATTR_HS_COLOR) + if hs_color is not None: + new_rgb = color_util.color_hs_to_RGB(*hs_color) + else: + new_rgb = None new_white = kwargs.get(ATTR_WHITE_VALUE) if new_rgb is None and new_white is None: From f8127a3902abaa6cb654ca7c28905adc81ee0623 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Wed, 21 Mar 2018 03:27:07 +0100 Subject: [PATCH 157/220] Add a polling fallback for Sonos (#13310) * Prepare for poll * Add a polling fallback for Sonos --- .../components/media_player/sonos.py | 280 +++++++++--------- tests/components/media_player/test_sonos.py | 6 +- 2 files changed, 151 insertions(+), 135 deletions(-) diff --git a/homeassistant/components/media_player/sonos.py b/homeassistant/components/media_player/sonos.py index 091046a6e7a..34ef146fc05 100644 --- a/homeassistant/components/media_player/sonos.py +++ b/homeassistant/components/media_player/sonos.py @@ -208,9 +208,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None): elif service.service == SERVICE_CLEAR_TIMER: device.clear_sleep_timer() elif service.service == SERVICE_UPDATE_ALARM: - device.update_alarm(**service.data) + device.set_alarm(**service.data) elif service.service == SERVICE_SET_OPTION: - device.update_option(**service.data) + device.set_option(**service.data) device.schedule_update_ha_state(True) @@ -330,12 +330,13 @@ class SonosDevice(MediaPlayerDevice): def __init__(self, player): """Initialize the Sonos device.""" + self._receives_events = False self._volume_increment = 5 self._unique_id = player.uid self._player = player self._model = None self._player_volume = None - self._player_volume_muted = None + self._player_muted = None self._play_mode = None self._name = None self._coordinator = None @@ -420,11 +421,9 @@ class SonosDevice(MediaPlayerDevice): speaker_info = self.soco.get_speaker_info(True) self._name = speaker_info['zone_name'] self._model = speaker_info['model_name'] - self._player_volume = self.soco.volume - self._player_volume_muted = self.soco.mute self._play_mode = self.soco.play_mode - self._night_sound = self.soco.night_mode - self._speech_enhance = self.soco.dialog_mode + + self.update_volume() self._favorites = [] for fav in self.soco.music_library.get_sonos_favorites(): @@ -437,124 +436,6 @@ class SonosDevice(MediaPlayerDevice): except Exception: _LOGGER.debug("Ignoring invalid favorite '%s'", fav.title) - def _subscribe_to_player_events(self): - """Add event subscriptions.""" - player = self.soco - - # New player available, build the current group topology - for device in self.hass.data[DATA_SONOS].devices: - device.process_zonegrouptopology_event(None) - - queue = _ProcessSonosEventQueue(self.process_avtransport_event) - player.avTransport.subscribe(auto_renew=True, event_queue=queue) - - queue = _ProcessSonosEventQueue(self.process_rendering_event) - player.renderingControl.subscribe(auto_renew=True, event_queue=queue) - - queue = _ProcessSonosEventQueue(self.process_zonegrouptopology_event) - player.zoneGroupTopology.subscribe(auto_renew=True, event_queue=queue) - - def update(self): - """Retrieve latest state.""" - available = self._check_available() - if self._available != available: - self._available = available - if available: - self._set_basic_information() - self._subscribe_to_player_events() - else: - self._player_volume = None - self._player_volume_muted = None - self._status = 'OFF' - self._coordinator = None - self._media_duration = None - self._media_position = None - self._media_position_updated_at = None - self._media_image_url = None - self._media_artist = None - self._media_album_name = None - self._media_title = None - self._source_name = None - - def process_avtransport_event(self, event): - """Process a track change event coming from a coordinator.""" - transport_info = self.soco.get_current_transport_info() - new_status = transport_info.get('current_transport_state') - - # Ignore transitions, we should get the target state soon - if new_status == 'TRANSITIONING': - return - - self._play_mode = self.soco.play_mode - - if self.soco.is_playing_tv: - self._refresh_linein(SOURCE_TV) - elif self.soco.is_playing_line_in: - self._refresh_linein(SOURCE_LINEIN) - else: - track_info = self.soco.get_current_track_info() - - if _is_radio_uri(track_info['uri']): - self._refresh_radio(event.variables, track_info) - else: - update_position = (new_status != self._status) - self._refresh_music(update_position, track_info) - - self._status = new_status - - self.schedule_update_ha_state() - - # Also update slaves - for entity in self.hass.data[DATA_SONOS].devices: - coordinator = entity.coordinator - if coordinator and coordinator.unique_id == self.unique_id: - entity.schedule_update_ha_state() - - def process_rendering_event(self, event): - """Process a volume change event coming from a player.""" - variables = event.variables - - if 'volume' in variables: - self._player_volume = int(variables['volume']['Master']) - - if 'mute' in variables: - self._player_volume_muted = (variables['mute']['Master'] == '1') - - if 'night_mode' in variables: - self._night_sound = (variables['night_mode'] == '1') - - if 'dialog_level' in variables: - self._speech_enhance = (variables['dialog_level'] == '1') - - self.schedule_update_ha_state() - - def process_zonegrouptopology_event(self, event): - """Process a zone group topology event coming from a player.""" - if event and not hasattr(event, 'zone_player_uui_ds_in_group'): - return - - with self.hass.data[DATA_SONOS].topology_lock: - group = event and event.zone_player_uui_ds_in_group - if group: - # New group information is pushed - coordinator_uid, *slave_uids = group.split(',') - else: - # Use SoCo cache for existing topology - coordinator_uid = self.soco.group.coordinator.uid - slave_uids = [p.uid for p in self.soco.group.members - if p.uid != coordinator_uid] - - if self.unique_id == coordinator_uid: - self._coordinator = None - self.schedule_update_ha_state() - - for slave_uid in slave_uids: - slave = _get_entity_from_soco_uid(self.hass, slave_uid) - if slave: - # pylint: disable=protected-access - slave._coordinator = self - slave.schedule_update_ha_state() - def _radio_artwork(self, url): """Return the private URL with artwork for a radio stream.""" if url not in ('', 'NOT_IMPLEMENTED', None): @@ -568,7 +449,88 @@ class SonosDevice(MediaPlayerDevice): ) return url - def _refresh_linein(self, source): + def _subscribe_to_player_events(self): + """Add event subscriptions.""" + self._receives_events = False + + # New player available, build the current group topology + for device in self.hass.data[DATA_SONOS].devices: + device.update_groups() + + player = self.soco + + queue = _ProcessSonosEventQueue(self.update_media) + player.avTransport.subscribe(auto_renew=True, event_queue=queue) + + queue = _ProcessSonosEventQueue(self.update_volume) + player.renderingControl.subscribe(auto_renew=True, event_queue=queue) + + queue = _ProcessSonosEventQueue(self.update_groups) + player.zoneGroupTopology.subscribe(auto_renew=True, event_queue=queue) + + def update(self): + """Retrieve latest state.""" + available = self._check_available() + if self._available != available: + self._available = available + if available: + self._set_basic_information() + self._subscribe_to_player_events() + else: + self._player_volume = None + self._player_muted = None + self._status = 'OFF' + self._coordinator = None + self._media_duration = None + self._media_position = None + self._media_position_updated_at = None + self._media_image_url = None + self._media_artist = None + self._media_album_name = None + self._media_title = None + self._source_name = None + elif available and not self._receives_events: + self.update_groups() + self.update_volume() + if self.is_coordinator: + self.update_media() + + def update_media(self, event=None): + """Update information about currently playing media.""" + transport_info = self.soco.get_current_transport_info() + new_status = transport_info.get('current_transport_state') + + # Ignore transitions, we should get the target state soon + if new_status == 'TRANSITIONING': + return + + self._play_mode = self.soco.play_mode + + if self.soco.is_playing_tv: + self.update_media_linein(SOURCE_TV) + elif self.soco.is_playing_line_in: + self.update_media_linein(SOURCE_LINEIN) + else: + track_info = self.soco.get_current_track_info() + + if _is_radio_uri(track_info['uri']): + variables = event and event.variables + self.update_media_radio(variables, track_info) + else: + update_position = (new_status != self._status) + self.update_media_music(update_position, track_info) + + self._status = new_status + + self.schedule_update_ha_state() + + # Also update slaves + for entity in self.hass.data[DATA_SONOS].devices: + coordinator = entity.coordinator + if coordinator and coordinator.unique_id == self.unique_id: + entity.schedule_update_ha_state() + + def update_media_linein(self, source): """Update state when playing from line-in/tv.""" self._media_duration = None self._media_position = None @@ -582,7 +544,7 @@ class SonosDevice(MediaPlayerDevice): self._source_name = source - def _refresh_radio(self, variables, track_info): + def update_media_radio(self, variables, track_info): """Update state when streaming radio.""" self._media_duration = None self._media_position = None @@ -603,7 +565,7 @@ class SonosDevice(MediaPlayerDevice): artist=self._media_artist, title=self._media_title ) - else: + elif variables: # "On Now" field in the sonos pc app current_track_metadata = variables.get('current_track_meta_data') if current_track_metadata: @@ -643,7 +605,7 @@ class SonosDevice(MediaPlayerDevice): if fav.reference.get_uri() == media_info['CurrentURI']: self._source_name = fav.title - def _refresh_music(self, update_media_position, track_info): + def update_media_music(self, update_media_position, track_info): """Update state when playing music tracks.""" self._media_duration = _timespan_secs(track_info.get('duration')) @@ -682,6 +644,60 @@ class SonosDevice(MediaPlayerDevice): self._source_name = None + def update_volume(self, event=None): + """Update information about currently volume settings.""" + if event: + variables = event.variables + + if 'volume' in variables: + self._player_volume = int(variables['volume']['Master']) + + if 'mute' in variables: + self._player_muted = (variables['mute']['Master'] == '1') + + if 'night_mode' in variables: + self._night_sound = (variables['night_mode'] == '1') + + if 'dialog_level' in variables: + self._speech_enhance = (variables['dialog_level'] == '1') + + self.schedule_update_ha_state() + else: + self._player_volume = self.soco.volume + self._player_muted = self.soco.mute + self._night_sound = self.soco.night_mode + self._speech_enhance = self.soco.dialog_mode + + def update_groups(self, event=None): + """Process a zone group topology event coming from a player.""" + if event: + self._receives_events = True + + if not hasattr(event, 'zone_player_uui_ds_in_group'): + return + + with self.hass.data[DATA_SONOS].topology_lock: + group = event and event.zone_player_uui_ds_in_group + if group: + # New group information is pushed + coordinator_uid, *slave_uids = group.split(',') + else: + # Use SoCo cache for existing topology + coordinator_uid = self.soco.group.coordinator.uid + slave_uids = [p.uid for p in self.soco.group.members + if p.uid != coordinator_uid] + + if self.unique_id == coordinator_uid: + self._coordinator = None + self.schedule_update_ha_state() + + for slave_uid in slave_uids: + slave = _get_entity_from_soco_uid(self.hass, slave_uid) + if slave: + # pylint: disable=protected-access + slave._coordinator = self + slave.schedule_update_ha_state() + @property def volume_level(self): """Volume level of the media player (0..1).""" @@ -690,7 +706,7 @@ class SonosDevice(MediaPlayerDevice): @property def is_volume_muted(self): """Return true if volume is muted.""" - return self._player_volume_muted + return self._player_muted @property @soco_coordinator @@ -988,7 +1004,7 @@ class SonosDevice(MediaPlayerDevice): @soco_error() @soco_coordinator - def update_alarm(self, **data): + def set_alarm(self, **data): """Set the alarm clock on the player.""" from soco import alarms alarm = None @@ -1011,7 +1027,7 @@ class SonosDevice(MediaPlayerDevice): alarm.save() @soco_error() - def update_option(self, **data): + def set_option(self, **data): """Modify playback options.""" if ATTR_NIGHT_SOUND in data and self._night_sound is not None: self.soco.night_mode = data[ATTR_NIGHT_SOUND] diff --git a/tests/components/media_player/test_sonos.py b/tests/components/media_player/test_sonos.py index f741898d15e..7d0d675f66f 100644 --- a/tests/components/media_player/test_sonos.py +++ b/tests/components/media_player/test_sonos.py @@ -276,7 +276,7 @@ class TestSonosMediaPlayer(unittest.TestCase): @mock.patch('soco.SoCo', new=SoCoMock) @mock.patch('soco.alarms.Alarm') @mock.patch('socket.create_connection', side_effect=socket.error()) - def test_update_alarm(self, soco_mock, alarm_mock, *args): + def test_set_alarm(self, soco_mock, alarm_mock, *args): """Ensuring soco methods called for sonos_set_sleep_timer service.""" sonos.setup_platform(self.hass, {}, add_devices_factory(self.hass), { 'host': '192.0.2.1' @@ -293,9 +293,9 @@ class TestSonosMediaPlayer(unittest.TestCase): 'include_linked_zones': True, 'volume': 0.30, } - device.update_alarm(alarm_id=2) + device.set_alarm(alarm_id=2) alarm1.save.assert_not_called() - device.update_alarm(alarm_id=1, **attrs) + device.set_alarm(alarm_id=1, **attrs) self.assertEqual(alarm1.enabled, attrs['enabled']) self.assertEqual(alarm1.start_time, attrs['time']) self.assertEqual(alarm1.include_linked_zones, From 74c249e57d16340ebd89fcd989942ff8b2fac26f Mon Sep 17 00:00:00 2001 From: Scott Reston Date: Wed, 21 Mar 2018 13:44:53 -0400 Subject: [PATCH 158/220] Fix retrieval of track URL into medi_content_id (#13333) 'current.item' was returning blank. --- homeassistant/components/media_player/spotify.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/media_player/spotify.py b/homeassistant/components/media_player/spotify.py index 734285d918a..963258f1861 100644 --- a/homeassistant/components/media_player/spotify.py +++ b/homeassistant/components/media_player/spotify.py @@ -194,7 +194,7 @@ class SpotifyMediaPlayer(MediaPlayerDevice): self._title = item.get('name') self._artist = ', '.join([artist.get('name') for artist in item.get('artists')]) - self._uri = current.get('uri') + self._uri = item.get('uri') images = item.get('album').get('images') self._image_url = images[0].get('url') if images else None # Playing state From 0396725fe965fc186aa39fc13f82e2de6e64b47b Mon Sep 17 00:00:00 2001 From: maxclaey Date: Wed, 21 Mar 2018 19:06:46 +0100 Subject: [PATCH 159/220] Homekit Bugfix: Use get instead of indexing (#13353) Fixes bug for alarm_control_panel if not code is required. --- homeassistant/components/homekit/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index b74171b08f7..02449607bf2 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -107,7 +107,8 @@ def get_accessory(hass, state, aid, config): _LOGGER.debug('Add "%s" as "%s"', state.entity_id, 'SecuritySystem') return TYPES['SecuritySystem'](hass, state.entity_id, state.name, - alarm_code=config[ATTR_CODE], aid=aid) + alarm_code=config.get(ATTR_CODE), + aid=aid) elif state.domain == 'climate': features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) From 36bc7f8175e8ecb11a9881ed2b39f8922c5c1716 Mon Sep 17 00:00:00 2001 From: maxclaey Date: Wed, 21 Mar 2018 20:29:58 +0100 Subject: [PATCH 160/220] Configuration options for IFTTT alarm control panel (#13352) * Improvements * Use optimistic instead of await callback * Fix default value for optimistic --- .../components/alarm_control_panel/ifttt.py | 48 ++++++++++++++----- 1 file changed, 36 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/alarm_control_panel/ifttt.py b/homeassistant/components/alarm_control_panel/ifttt.py index eb1a8f8ed7d..5303c24876e 100644 --- a/homeassistant/components/alarm_control_panel/ifttt.py +++ b/homeassistant/components/alarm_control_panel/ifttt.py @@ -15,7 +15,7 @@ from homeassistant.components.ifttt import ( ATTR_EVENT, DOMAIN as IFTTT_DOMAIN, SERVICE_TRIGGER) from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_STATE, CONF_NAME, CONF_CODE, - STATE_ALARM_DISARMED, STATE_ALARM_ARMED_NIGHT, + CONF_OPTIMISTIC, STATE_ALARM_DISARMED, STATE_ALARM_ARMED_NIGHT, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_AWAY) import homeassistant.helpers.config_validation as cv @@ -30,14 +30,24 @@ ALLOWED_STATES = [ DATA_IFTTT_ALARM = 'ifttt_alarm' DEFAULT_NAME = "Home" -EVENT_ALARM_ARM_AWAY = "alarm_arm_away" -EVENT_ALARM_ARM_HOME = "alarm_arm_home" -EVENT_ALARM_ARM_NIGHT = "alarm_arm_night" -EVENT_ALARM_DISARM = "alarm_disarm" +CONF_EVENT_AWAY = "event_arm_away" +CONF_EVENT_HOME = "event_arm_home" +CONF_EVENT_NIGHT = "event_arm_night" +CONF_EVENT_DISARM = "event_disarm" + +DEFAULT_EVENT_AWAY = "alarm_arm_away" +DEFAULT_EVENT_HOME = "alarm_arm_home" +DEFAULT_EVENT_NIGHT = "alarm_arm_night" +DEFAULT_EVENT_DISARM = "alarm_disarm" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_CODE): cv.string, + vol.Optional(CONF_EVENT_AWAY, default=DEFAULT_EVENT_AWAY): cv.string, + vol.Optional(CONF_EVENT_HOME, default=DEFAULT_EVENT_HOME): cv.string, + vol.Optional(CONF_EVENT_NIGHT, default=DEFAULT_EVENT_NIGHT): cv.string, + vol.Optional(CONF_EVENT_DISARM, default=DEFAULT_EVENT_DISARM): cv.string, + vol.Optional(CONF_OPTIMISTIC, default=False): cv.boolean, }) SERVICE_PUSH_ALARM_STATE = "ifttt_push_alarm_state" @@ -55,8 +65,14 @@ def setup_platform(hass, config, add_devices, discovery_info=None): name = config.get(CONF_NAME) code = config.get(CONF_CODE) + event_away = config.get(CONF_EVENT_AWAY) + event_home = config.get(CONF_EVENT_HOME) + event_night = config.get(CONF_EVENT_NIGHT) + event_disarm = config.get(CONF_EVENT_DISARM) + optimistic = config.get(CONF_OPTIMISTIC) - alarmpanel = IFTTTAlarmPanel(name, code) + alarmpanel = IFTTTAlarmPanel(name, code, event_away, event_home, + event_night, event_disarm, optimistic) hass.data[DATA_IFTTT_ALARM].append(alarmpanel) add_devices([alarmpanel]) @@ -79,10 +95,16 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class IFTTTAlarmPanel(alarm.AlarmControlPanel): """Representation of an alarm control panel controlled throught IFTTT.""" - def __init__(self, name, code): + def __init__(self, name, code, event_away, event_home, event_night, + event_disarm, optimistic): """Initialize the alarm control panel.""" self._name = name self._code = code + self._event_away = event_away + self._event_home = event_home + self._event_night = event_night + self._event_disarm = event_disarm + self._optimistic = optimistic self._state = None @property @@ -109,32 +131,34 @@ class IFTTTAlarmPanel(alarm.AlarmControlPanel): """Send disarm command.""" if not self._check_code(code): return - self.set_alarm_state(EVENT_ALARM_DISARM) + self.set_alarm_state(self._event_disarm, STATE_ALARM_DISARMED) def alarm_arm_away(self, code=None): """Send arm away command.""" if not self._check_code(code): return - self.set_alarm_state(EVENT_ALARM_ARM_AWAY) + self.set_alarm_state(self._event_away, STATE_ALARM_ARMED_AWAY) def alarm_arm_home(self, code=None): """Send arm home command.""" if not self._check_code(code): return - self.set_alarm_state(EVENT_ALARM_ARM_HOME) + self.set_alarm_state(self._event_home, STATE_ALARM_ARMED_HOME) def alarm_arm_night(self, code=None): """Send arm night command.""" if not self._check_code(code): return - self.set_alarm_state(EVENT_ALARM_ARM_NIGHT) + self.set_alarm_state(self._event_night, STATE_ALARM_ARMED_NIGHT) - def set_alarm_state(self, event): + def set_alarm_state(self, event, state): """Call the IFTTT trigger service to change the alarm state.""" data = {ATTR_EVENT: event} self.hass.services.call(IFTTT_DOMAIN, SERVICE_TRIGGER, data) _LOGGER.debug("Called IFTTT component to trigger event %s", event) + if self._optimistic: + self._state = state def push_alarm_state(self, value): """Push the alarm state to the given value.""" From 39394608143c864340645683af344be2195a2f7c Mon Sep 17 00:00:00 2001 From: Thomas Svedberg <36861881+ThomasSvedberg@users.noreply.github.com> Date: Wed, 21 Mar 2018 23:21:51 +0100 Subject: [PATCH 161/220] =?UTF-8?q?Add=20the=20possibility=20to=20filter?= =?UTF-8?q?=20on=20line(s)=20in=20V=C3=A4sttrafik=20Public=20Transport=20s?= =?UTF-8?q?ensor=20(#13317)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add the possibility to filter on line(s) in Västtrafik Public Transport sensor Add a config entry "lines" to be able to filter departures on line(s) for the vasttrafik sensor. * Change log level to debug if no departures found. * Remove extra None argument from dict().get() calls as it is already the default. * Ensure "lines" is a list of strings. Also fix an indentation error. * Correct to long line --- homeassistant/components/sensor/vasttrafik.py | 45 +++++++++++++------ 1 file changed, 31 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/sensor/vasttrafik.py b/homeassistant/components/sensor/vasttrafik.py index 983c589c98b..8cd084e1b71 100644 --- a/homeassistant/components/sensor/vasttrafik.py +++ b/homeassistant/components/sensor/vasttrafik.py @@ -30,6 +30,7 @@ CONF_DELAY = 'delay' CONF_DEPARTURES = 'departures' CONF_FROM = 'from' CONF_HEADING = 'heading' +CONF_LINES = 'lines' CONF_KEY = 'key' CONF_SECRET = 'secret' @@ -46,6 +47,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_FROM): cv.string, vol.Optional(CONF_DELAY, default=DEFAULT_DELAY): cv.positive_int, vol.Optional(CONF_HEADING): cv.string, + vol.Optional(CONF_LINES, default=[]): + vol.All(cv.ensure_list, [cv.string]), vol.Optional(CONF_NAME): cv.string}] }) @@ -61,14 +64,15 @@ def setup_platform(hass, config, add_devices, discovery_info=None): VasttrafikDepartureSensor( vasttrafik, planner, departure.get(CONF_NAME), departure.get(CONF_FROM), departure.get(CONF_HEADING), - departure.get(CONF_DELAY))) + departure.get(CONF_LINES), departure.get(CONF_DELAY))) add_devices(sensors, True) class VasttrafikDepartureSensor(Entity): """Implementation of a Vasttrafik Departure Sensor.""" - def __init__(self, vasttrafik, planner, name, departure, heading, delay): + def __init__(self, vasttrafik, planner, name, departure, heading, + lines, delay): """Initialize the sensor.""" self._vasttrafik = vasttrafik self._planner = planner @@ -76,6 +80,7 @@ class VasttrafikDepartureSensor(Entity): self._departure = planner.location_name(departure)[0] self._heading = (planner.location_name(heading)[0] if heading else None) + self._lines = lines if lines else None self._delay = timedelta(minutes=delay) self._departureboard = None @@ -94,15 +99,18 @@ class VasttrafikDepartureSensor(Entity): """Return the state attributes.""" if not self._departureboard: return - departure = self._departureboard[0] - params = { - ATTR_ACCESSIBILITY: departure.get('accessibility', None), - ATTR_ATTRIBUTION: CONF_ATTRIBUTION, - ATTR_DIRECTION: departure.get('direction', None), - ATTR_LINE: departure.get('sname', None), - ATTR_TRACK: departure.get('track', None), - } - return {k: v for k, v in params.items() if v} + + for departure in self._departureboard: + line = departure.get('sname') + if not self._lines or line in self._lines: + params = { + ATTR_ACCESSIBILITY: departure.get('accessibility'), + ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + ATTR_DIRECTION: departure.get('direction'), + ATTR_LINE: departure.get('sname'), + ATTR_TRACK: departure.get('track'), + } + return {k: v for k, v in params.items() if v} @property def state(self): @@ -113,9 +121,18 @@ class VasttrafikDepartureSensor(Entity): self._departure['name'], self._heading['name'] if self._heading else 'ANY') return - if 'rtTime' in self._departureboard[0]: - return self._departureboard[0]['rtTime'] - return self._departureboard[0]['time'] + for departure in self._departureboard: + line = departure.get('sname') + if not self._lines or line in self._lines: + if 'rtTime' in self._departureboard[0]: + return self._departureboard[0]['rtTime'] + return self._departureboard[0]['time'] + # No departures of given lines found + _LOGGER.debug( + "No departures from %s heading %s on line(s) %s", + self._departure['name'], + self._heading['name'] if self._heading else 'ANY', + ', '.join((str(line) for line in self._lines))) @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): From 2d7d8848cba8f6e78b8b27c14197e344d356c189 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Wed, 21 Mar 2018 23:48:50 +0100 Subject: [PATCH 162/220] Fix mysensors RGBW (#13364) Tuple doesn't have append method. --- homeassistant/components/light/mysensors.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/light/mysensors.py b/homeassistant/components/light/mysensors.py index d595fc4b184..14a770b7632 100644 --- a/homeassistant/components/light/mysensors.py +++ b/homeassistant/components/light/mysensors.py @@ -104,7 +104,7 @@ class MySensorsLight(mysensors.MySensorsEntity, Light): def _turn_on_rgb_and_w(self, hex_template, **kwargs): """Turn on RGB or RGBW child device.""" - rgb = color_util.color_hs_to_RGB(*self._hs) + rgb = list(color_util.color_hs_to_RGB(*self._hs)) white = self._white hex_color = self._values.get(self.value_type) hs_color = kwargs.get(ATTR_HS_COLOR) From 17cbd0f3c9d9d5d3ef2caa7d15ada64266206c94 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Wed, 21 Mar 2018 23:55:49 +0100 Subject: [PATCH 163/220] Add watt to mysensors switch attributes (#13370) --- homeassistant/components/switch/mysensors.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/homeassistant/components/switch/mysensors.py b/homeassistant/components/switch/mysensors.py index 51184859fc6..b4a1dcde3e6 100644 --- a/homeassistant/components/switch/mysensors.py +++ b/homeassistant/components/switch/mysensors.py @@ -72,6 +72,12 @@ class MySensorsSwitch(mysensors.MySensorsEntity, SwitchDevice): """Return True if unable to access real state of entity.""" return self.gateway.optimistic + @property + def current_power_w(self): + """Return the current power usage in W.""" + set_req = self.gateway.const.SetReq + return self._values.get(set_req.V_WATT) + @property def is_on(self): """Return True if switch is on.""" From 1676df6a5f9af8767713eaffa6f4dce23e8d488e Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Thu, 22 Mar 2018 01:06:41 +0000 Subject: [PATCH 164/220] Mediaroom async (#13321) * Initial commit on new asyncio based version * Async version * Lint * updated to lasted pymediaroom version * bump version * optimistic state updates * bump version * Moved class import to method import * async schedule and name correction * Addresses @balloob comments * missed fixes * no unique_id for configuration based STB * hound * handle 2 mediaroom platforms --- .../components/media_player/mediaroom.py | 340 ++++++++++++------ requirements_all.txt | 2 +- 2 files changed, 230 insertions(+), 112 deletions(-) diff --git a/homeassistant/components/media_player/mediaroom.py b/homeassistant/components/media_player/mediaroom.py index 3cf0ecdb232..a6d5841bb0f 100644 --- a/homeassistant/components/media_player/mediaroom.py +++ b/homeassistant/components/media_player/mediaroom.py @@ -9,134 +9,182 @@ import logging import voluptuous as vol from homeassistant.components.media_player import ( - MEDIA_TYPE_CHANNEL, SUPPORT_PAUSE, SUPPORT_PLAY_MEDIA, - SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_STOP, PLATFORM_SCHEMA, - SUPPORT_NEXT_TRACK, SUPPORT_PREVIOUS_TRACK, SUPPORT_PLAY, - SUPPORT_VOLUME_STEP, SUPPORT_VOLUME_MUTE, - MediaPlayerDevice) + MEDIA_TYPE_CHANNEL, SUPPORT_PAUSE, SUPPORT_PLAY_MEDIA, SUPPORT_TURN_OFF, + SUPPORT_TURN_ON, SUPPORT_STOP, PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK, + SUPPORT_PREVIOUS_TRACK, SUPPORT_PLAY, SUPPORT_VOLUME_STEP, + SUPPORT_VOLUME_MUTE, MediaPlayerDevice, +) +from homeassistant.helpers.dispatcher import ( + dispatcher_send, async_dispatcher_connect +) from homeassistant.const import ( - CONF_HOST, CONF_NAME, CONF_OPTIMISTIC, CONF_TIMEOUT, - STATE_PAUSED, STATE_PLAYING, STATE_STANDBY, - STATE_ON) + CONF_HOST, CONF_NAME, CONF_OPTIMISTIC, STATE_OFF, + CONF_TIMEOUT, STATE_PAUSED, STATE_PLAYING, STATE_STANDBY, + STATE_UNAVAILABLE +) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['pymediaroom==0.5'] + +REQUIREMENTS = ['pymediaroom==0.6'] _LOGGER = logging.getLogger(__name__) -NOTIFICATION_TITLE = 'Mediaroom Media Player Setup' -NOTIFICATION_ID = 'mediaroom_notification' DEFAULT_NAME = 'Mediaroom STB' DEFAULT_TIMEOUT = 9 DATA_MEDIAROOM = "mediaroom_known_stb" +DISCOVERY_MEDIAROOM = "mediaroom_discovery_installed" +SIGNAL_STB_NOTIFY = 'mediaroom_stb_discovered' +SUPPORT_MEDIAROOM = SUPPORT_PAUSE | SUPPORT_TURN_ON | SUPPORT_TURN_OFF \ + | SUPPORT_VOLUME_STEP | SUPPORT_VOLUME_MUTE | SUPPORT_PLAY_MEDIA \ + | SUPPORT_STOP | SUPPORT_NEXT_TRACK | SUPPORT_PREVIOUS_TRACK \ + | SUPPORT_PLAY -SUPPORT_MEDIAROOM = SUPPORT_PAUSE | SUPPORT_TURN_ON | SUPPORT_TURN_OFF | \ - SUPPORT_VOLUME_STEP | SUPPORT_VOLUME_MUTE | \ - SUPPORT_PLAY_MEDIA | SUPPORT_STOP | SUPPORT_NEXT_TRACK | \ - SUPPORT_PREVIOUS_TRACK | SUPPORT_PLAY - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_HOST): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_OPTIMISTIC, default=False): cv.boolean, - vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_HOST): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_OPTIMISTIC, default=False): cv.boolean, + vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, + } +) -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 the Mediaroom platform.""" - hosts = [] - known_hosts = hass.data.get(DATA_MEDIAROOM) if known_hosts is None: known_hosts = hass.data[DATA_MEDIAROOM] = [] - host = config.get(CONF_HOST, None) - if host is None: - _LOGGER.info("Trying to discover Mediaroom STB") + if host: + async_add_devices([MediaroomDevice(host=host, + device_id=None, + optimistic=config[CONF_OPTIMISTIC], + timeout=config[CONF_TIMEOUT])]) + hass.data[DATA_MEDIAROOM].append(host) - from pymediaroom import Remote + _LOGGER.debug("Trying to discover Mediaroom STB") - host = Remote.discover(known_hosts) - if host is None: - _LOGGER.warning("Can't find any STB") + def callback_notify(notify): + """Process NOTIFY message from STB.""" + if notify.ip_address in hass.data[DATA_MEDIAROOM]: + dispatcher_send(hass, SIGNAL_STB_NOTIFY, notify) return - hosts.append(host) - known_hosts.append(host) - stbs = [] + _LOGGER.debug("Discovered new stb %s", notify.ip_address) + hass.data[DATA_MEDIAROOM].append(notify.ip_address) + new_stb = MediaroomDevice( + host=notify.ip_address, device_id=notify.device_uuid, + optimistic=False + ) + async_add_devices([new_stb]) - try: - for host in hosts: - stbs.append(MediaroomDevice( - config.get(CONF_NAME), - host, - config.get(CONF_OPTIMISTIC), - config.get(CONF_TIMEOUT) - )) + if not config[CONF_OPTIMISTIC]: + from pymediaroom import install_mediaroom_protocol - except ConnectionRefusedError: - hass.components.persistent_notification.create( - 'Error: Unable to initialize mediaroom at {}
' - 'Check its network connection or consider ' - 'using auto discovery.
' - 'You will need to restart hass after fixing.' - ''.format(host), - title=NOTIFICATION_TITLE, - notification_id=NOTIFICATION_ID) - - add_devices(stbs) + already_installed = hass.data.get(DISCOVERY_MEDIAROOM, False) + if not already_installed: + await install_mediaroom_protocol( + responses_callback=callback_notify) + _LOGGER.debug("Auto discovery installed") + hass.data[DISCOVERY_MEDIAROOM] = True class MediaroomDevice(MediaPlayerDevice): """Representation of a Mediaroom set-up-box on the network.""" - def __init__(self, name, host, optimistic=False, timeout=DEFAULT_TIMEOUT): + def set_state(self, mediaroom_state): + """Helper method to map pymediaroom states to HA states.""" + from pymediaroom import State + + state_map = { + State.OFF: STATE_OFF, + State.STANDBY: STATE_STANDBY, + State.PLAYING_LIVE_TV: STATE_PLAYING, + State.PLAYING_RECORDED_TV: STATE_PLAYING, + State.PLAYING_TIMESHIFT_TV: STATE_PLAYING, + State.STOPPED: STATE_PAUSED, + State.UNKNOWN: STATE_UNAVAILABLE + } + + self._state = state_map[mediaroom_state] + + def __init__(self, host, device_id, optimistic=False, + timeout=DEFAULT_TIMEOUT): """Initialize the device.""" from pymediaroom import Remote - self.stb = Remote(host, timeout=timeout) - _LOGGER.info( - "Found %s at %s%s", name, host, - " - I'm optimistic" if optimistic else "") - self._name = name - self._is_standby = not optimistic - self._current = None + self.host = host + self.stb = Remote(host) + _LOGGER.info("Found STB at %s%s", host, + " - I'm optimistic" if optimistic else "") + self._channel = None self._optimistic = optimistic - self._state = STATE_STANDBY + self._state = STATE_PLAYING if optimistic else STATE_STANDBY + self._name = 'Mediaroom {}'.format(device_id) + self._available = True + if device_id: + self._unique_id = device_id + else: + self._unique_id = None - def update(self): + @property + def should_poll(self): + """No polling needed.""" + return False + + @property + def available(self): + """Return True if entity is available.""" + return self._available + + async def async_added_to_hass(self): """Retrieve latest state.""" - if not self._optimistic: - self._is_standby = self.stb.get_standby() - if self._is_standby: - self._state = STATE_STANDBY - elif self._state not in [STATE_PLAYING, STATE_PAUSED]: - self._state = STATE_PLAYING - _LOGGER.debug( - "%s(%s) is [%s]", - self._name, self.stb.stb_ip, self._state) + async def async_notify_received(notify): + """Process STB state from NOTIFY message.""" + stb_state = self.stb.notify_callback(notify) + # stb_state is None in case the notify is not from the current stb + if not stb_state: + return + self.set_state(stb_state) + _LOGGER.debug("STB(%s) is [%s]", self.host, self._state) + self._available = True + self.async_schedule_update_ha_state() - def play_media(self, media_type, media_id, **kwargs): + async_dispatcher_connect(self.hass, SIGNAL_STB_NOTIFY, + async_notify_received) + + async def async_play_media(self, media_type, media_id, **kwargs): """Play media.""" - _LOGGER.debug( - "%s(%s) Play media: %s (%s)", - self._name, self.stb.stb_ip, media_id, media_type) + from pymediaroom import PyMediaroomError + + _LOGGER.debug("STB(%s) Play media: %s (%s)", self.stb.stb_ip, + media_id, media_type) if media_type != MEDIA_TYPE_CHANNEL: _LOGGER.error('invalid media type') return - if media_id.isdigit(): - media_id = int(media_id) - else: + if not media_id.isdigit(): + _LOGGER.error("media_id must be a channel number") return - self.stb.send_cmd(media_id) - self._state = STATE_PLAYING + + try: + await self.stb.send_cmd(int(media_id)) + if self._optimistic: + self._state = STATE_PLAYING + self._available = True + except PyMediaroomError: + self._available = False + self.async_schedule_update_ha_state() + + @property + def unique_id(self): + """Return a unique ID.""" + return self._unique_id @property def name(self): """Return the name of the device.""" return self._name - # MediaPlayerDevice properties and methods @property def state(self): """Return the state of the device.""" @@ -152,50 +200,120 @@ class MediaroomDevice(MediaPlayerDevice): """Return the content type of current playing media.""" return MEDIA_TYPE_CHANNEL - def turn_on(self): + @property + def media_channel(self): + """Channel currently playing.""" + return self._channel + + async def async_turn_on(self): """Turn on the receiver.""" - self.stb.send_cmd('Power') - self._state = STATE_ON + from pymediaroom import PyMediaroomError + try: + self.set_state(await self.stb.turn_on()) + if self._optimistic: + self._state = STATE_PLAYING + self._available = True + except PyMediaroomError: + self._available = False + self.async_schedule_update_ha_state() - def turn_off(self): + async def async_turn_off(self): """Turn off the receiver.""" - self.stb.send_cmd('Power') - self._state = STATE_STANDBY + from pymediaroom import PyMediaroomError + try: + self.set_state(await self.stb.turn_off()) + if self._optimistic: + self._state = STATE_STANDBY + self._available = True + except PyMediaroomError: + self._available = False + self.async_schedule_update_ha_state() - def media_play(self): + async def async_media_play(self): """Send play command.""" - _LOGGER.debug("media_play()") - self.stb.send_cmd('PlayPause') - self._state = STATE_PLAYING + from pymediaroom import PyMediaroomError + try: + _LOGGER.debug("media_play()") + await self.stb.send_cmd('PlayPause') + if self._optimistic: + self._state = STATE_PLAYING + self._available = True + except PyMediaroomError: + self._available = False + self.async_schedule_update_ha_state() - def media_pause(self): + async def async_media_pause(self): """Send pause command.""" - self.stb.send_cmd('PlayPause') - self._state = STATE_PAUSED + from pymediaroom import PyMediaroomError + try: + await self.stb.send_cmd('PlayPause') + if self._optimistic: + self._state = STATE_PAUSED + self._available = True + except PyMediaroomError: + self._available = False + self.async_schedule_update_ha_state() - def media_stop(self): + async def async_media_stop(self): """Send stop command.""" - self.stb.send_cmd('Stop') - self._state = STATE_PAUSED + from pymediaroom import PyMediaroomError + try: + await self.stb.send_cmd('Stop') + if self._optimistic: + self._state = STATE_PAUSED + self._available = True + except PyMediaroomError: + self._available = False + self.async_schedule_update_ha_state() - def media_previous_track(self): + async def async_media_previous_track(self): """Send Program Down command.""" - self.stb.send_cmd('ProgDown') - self._state = STATE_PLAYING + from pymediaroom import PyMediaroomError + try: + await self.stb.send_cmd('ProgDown') + if self._optimistic: + self._state = STATE_PLAYING + self._available = True + except PyMediaroomError: + self._available = False + self.async_schedule_update_ha_state() - def media_next_track(self): + async def async_media_next_track(self): """Send Program Up command.""" - self.stb.send_cmd('ProgUp') - self._state = STATE_PLAYING + from pymediaroom import PyMediaroomError + try: + await self.stb.send_cmd('ProgUp') + if self._optimistic: + self._state = STATE_PLAYING + self._available = True + except PyMediaroomError: + self._available = False + self.async_schedule_update_ha_state() - def volume_up(self): + async def async_volume_up(self): """Send volume up command.""" - self.stb.send_cmd('VolUp') + from pymediaroom import PyMediaroomError + try: + await self.stb.send_cmd('VolUp') + self._available = True + except PyMediaroomError: + self._available = False + self.async_schedule_update_ha_state() - def volume_down(self): + async def async_volume_down(self): """Send volume up command.""" - self.stb.send_cmd('VolDown') + from pymediaroom import PyMediaroomError + try: + await self.stb.send_cmd('VolDown') + except PyMediaroomError: + self._available = False + self.async_schedule_update_ha_state() - def mute_volume(self, mute): + async def async_mute_volume(self, mute): """Send mute command.""" - self.stb.send_cmd('Mute') + from pymediaroom import PyMediaroomError + try: + await self.stb.send_cmd('Mute') + except PyMediaroomError: + self._available = False + self.async_schedule_update_ha_state() diff --git a/requirements_all.txt b/requirements_all.txt index f3637a40f64..0c68d6a29ae 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -804,7 +804,7 @@ pylutron==0.1.0 pymailgunner==1.4 # homeassistant.components.media_player.mediaroom -pymediaroom==0.5 +pymediaroom==0.6 # homeassistant.components.media_player.xiaomi_tv pymitv==1.0.0 From 6e75c5427cec9e97624368befadd4eb819799d5c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 21 Mar 2018 21:22:16 -0700 Subject: [PATCH 165/220] Update frontend to 20180322.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 eccc47e05c7..9107e64a040 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -24,7 +24,7 @@ from homeassistant.core import callback from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass -REQUIREMENTS = ['home-assistant-frontend==20180316.0'] +REQUIREMENTS = ['home-assistant-frontend==20180322.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index 0c68d6a29ae..793403f26d7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -356,7 +356,7 @@ hipnotify==1.0.8 holidays==0.9.4 # homeassistant.components.frontend -home-assistant-frontend==20180316.0 +home-assistant-frontend==20180322.0 # homeassistant.components.homematicip_cloud homematicip==0.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0a7c3b493d4..d2c1df2d3bf 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.1 holidays==0.9.4 # homeassistant.components.frontend -home-assistant-frontend==20180316.0 +home-assistant-frontend==20180322.0 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From e9cdbe5d8c5b659a3c80dbd1346192418a6839d8 Mon Sep 17 00:00:00 2001 From: cgtobi Date: Thu, 22 Mar 2018 13:34:02 +0100 Subject: [PATCH 166/220] Add language parameter to darksky sensor (#13297) --- .coveragerc | 1 - homeassistant/components/sensor/darksky.py | 24 ++++- tests/components/sensor/test_darksky.py | 120 ++++++++++++++++++--- 3 files changed, 125 insertions(+), 20 deletions(-) diff --git a/.coveragerc b/.coveragerc index 1dcde0ded14..a2c0dde77b1 100644 --- a/.coveragerc +++ b/.coveragerc @@ -561,7 +561,6 @@ omit = homeassistant/components/sensor/crimereports.py homeassistant/components/sensor/cups.py homeassistant/components/sensor/currencylayer.py - homeassistant/components/sensor/darksky.py homeassistant/components/sensor/deluge.py homeassistant/components/sensor/deutsche_bahn.py homeassistant/components/sensor/dht.py diff --git a/homeassistant/components/sensor/darksky.py b/homeassistant/components/sensor/darksky.py index 3049415c754..261e0a62409 100644 --- a/homeassistant/components/sensor/darksky.py +++ b/homeassistant/components/sensor/darksky.py @@ -27,6 +27,9 @@ CONF_ATTRIBUTION = "Powered by Dark Sky" CONF_UNITS = 'units' CONF_UPDATE_INTERVAL = 'update_interval' CONF_FORECAST = 'forecast' +CONF_LANGUAGE = 'language' + +DEFAULT_LANGUAGE = 'en' DEFAULT_NAME = 'Dark Sky' @@ -118,6 +121,16 @@ CONDITION_PICTURES = { 'partly-cloudy-night': '/static/images/darksky/weather-cloudy.svg', } +# Language Supported Codes +LANGUAGE_CODES = [ + 'ar', 'az', 'be', 'bg', 'bs', 'ca', + 'cs', 'da', 'de', 'el', 'en', 'es', + 'et', 'fi', 'fr', 'hr', 'hu', 'id', + 'is', 'it', 'ja', 'ka', 'kw', 'nb', + 'nl', 'pl', 'pt', 'ro', 'ru', 'sk', + 'sl', 'sr', 'sv', 'tet', 'tr', 'uk', + 'x-pig-latin', 'zh', 'zh-tw', +] PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_MONITORED_CONDITIONS): @@ -125,6 +138,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_API_KEY): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_UNITS): vol.In(['auto', 'si', 'us', 'ca', 'uk', 'uk2']), + vol.Optional(CONF_LANGUAGE, + default=DEFAULT_LANGUAGE): vol.In(LANGUAGE_CODES), vol.Inclusive(CONF_LATITUDE, 'coordinates', 'Latitude and longitude must exist together'): cv.latitude, vol.Inclusive(CONF_LONGITUDE, 'coordinates', @@ -140,6 +155,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Dark Sky sensor.""" latitude = config.get(CONF_LATITUDE, hass.config.latitude) longitude = config.get(CONF_LONGITUDE, hass.config.longitude) + language = config.get(CONF_LANGUAGE) if CONF_UNITS in config: units = config[CONF_UNITS] @@ -153,6 +169,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): latitude=latitude, longitude=longitude, units=units, + language=language, interval=config.get(CONF_UPDATE_INTERVAL)) forecast_data.update() forecast_data.update_currently() @@ -332,12 +349,14 @@ def convert_to_camel(data): class DarkSkyData(object): """Get the latest data from Darksky.""" - def __init__(self, api_key, latitude, longitude, units, interval): + def __init__(self, api_key, latitude, longitude, units, language, + interval): """Initialize the data object.""" self._api_key = api_key self.latitude = latitude self.longitude = longitude self.units = units + self.language = language self.data = None self.unit_system = None @@ -359,7 +378,8 @@ class DarkSkyData(object): try: self.data = forecastio.load_forecast( - self._api_key, self.latitude, self.longitude, units=self.units) + self._api_key, self.latitude, self.longitude, units=self.units, + lang=self.language) except (ConnectError, HTTPError, Timeout, ValueError) as error: _LOGGER.error("Unable to connect to Dark Sky. %s", error) self.data = None diff --git a/tests/components/sensor/test_darksky.py b/tests/components/sensor/test_darksky.py index 7ee04b0df4c..9300ecef432 100644 --- a/tests/components/sensor/test_darksky.py +++ b/tests/components/sensor/test_darksky.py @@ -2,16 +2,69 @@ import re import unittest from unittest.mock import MagicMock, patch +from datetime import timedelta -import forecastio from requests.exceptions import HTTPError import requests_mock -from datetime import timedelta + +import forecastio from homeassistant.components.sensor import darksky from homeassistant.setup import setup_component -from tests.common import load_fixture, get_test_home_assistant +from tests.common import (load_fixture, get_test_home_assistant, + MockDependency) + +VALID_CONFIG_MINIMAL = { + 'sensor': { + 'platform': 'darksky', + 'api_key': 'foo', + 'forecast': [1, 2], + 'monitored_conditions': ['summary', 'icon', 'temperature_max'], + 'update_interval': timedelta(seconds=120), + } +} + +INVALID_CONFIG_MINIMAL = { + 'sensor': { + 'platform': 'darksky', + 'api_key': 'foo', + 'forecast': [1, 2], + 'monitored_conditions': ['sumary', 'iocn', 'temperature_max'], + 'update_interval': timedelta(seconds=120), + } +} + +VALID_CONFIG_LANG_DE = { + 'sensor': { + 'platform': 'darksky', + 'api_key': 'foo', + 'forecast': [1, 2], + 'units': 'us', + 'language': 'de', + 'monitored_conditions': ['summary', 'icon', 'temperature_max', + 'minutely_summary', 'hourly_summary', + 'daily_summary', 'humidity', ], + 'update_interval': timedelta(seconds=120), + } +} + +INVALID_CONFIG_LANG = { + 'sensor': { + 'platform': 'darksky', + 'api_key': 'foo', + 'forecast': [1, 2], + 'language': 'yz', + 'monitored_conditions': ['summary', 'icon', 'temperature_max'], + 'update_interval': timedelta(seconds=120), + } +} + + +def load_forecastMock(key, lat, lon, + units, lang): # pylint: disable=invalid-name + """Mock darksky forecast loading.""" + return '' class TestDarkSkySetup(unittest.TestCase): @@ -30,12 +83,6 @@ class TestDarkSkySetup(unittest.TestCase): """Initialize values for this testcase class.""" self.hass = get_test_home_assistant() self.key = 'foo' - self.config = { - 'api_key': 'foo', - 'forecast': [1, 2], - 'monitored_conditions': ['summary', 'icon', 'temperature_max'], - 'update_interval': timedelta(seconds=120), - } self.lat = self.hass.config.latitude = 37.8267 self.lon = self.hass.config.longitude = -122.423 self.entities = [] @@ -44,10 +91,41 @@ class TestDarkSkySetup(unittest.TestCase): """Stop everything that was started.""" self.hass.stop() - def test_setup_with_config(self): + @MockDependency('forecastio') + @patch('forecastio.load_forecast', new=load_forecastMock) + def test_setup_with_config(self, mock_forecastio): """Test the platform setup with configuration.""" - self.assertTrue( - setup_component(self.hass, 'sensor', {'darksky': self.config})) + setup_component(self.hass, 'sensor', VALID_CONFIG_MINIMAL) + + state = self.hass.states.get('sensor.dark_sky_summary') + assert state is not None + + @MockDependency('forecastio') + @patch('forecastio.load_forecast', new=load_forecastMock) + def test_setup_with_invalid_config(self, mock_forecastio): + """Test the platform setup with invalid configuration.""" + setup_component(self.hass, 'sensor', INVALID_CONFIG_MINIMAL) + + state = self.hass.states.get('sensor.dark_sky_summary') + assert state is None + + @MockDependency('forecastio') + @patch('forecastio.load_forecast', new=load_forecastMock) + def test_setup_with_language_config(self, mock_forecastio): + """Test the platform setup with language configuration.""" + setup_component(self.hass, 'sensor', VALID_CONFIG_LANG_DE) + + state = self.hass.states.get('sensor.dark_sky_summary') + assert state is not None + + @MockDependency('forecastio') + @patch('forecastio.load_forecast', new=load_forecastMock) + def test_setup_with_invalid_language_config(self, mock_forecastio): + """Test the platform setup with language configuration.""" + setup_component(self.hass, 'sensor', INVALID_CONFIG_LANG) + + state = self.hass.states.get('sensor.dark_sky_summary') + assert state is None @patch('forecastio.api.get_forecast') def test_setup_bad_api_key(self, mock_get_forecast): @@ -60,7 +138,8 @@ class TestDarkSkySetup(unittest.TestCase): msg = '400 Client Error: Bad Request for url: {}'.format(url) mock_get_forecast.side_effect = HTTPError(msg,) - response = darksky.setup_platform(self.hass, self.config, MagicMock()) + response = darksky.setup_platform(self.hass, VALID_CONFIG_MINIMAL, + MagicMock()) self.assertFalse(response) @requests_mock.Mocker() @@ -69,9 +148,16 @@ class TestDarkSkySetup(unittest.TestCase): """Test for successfully setting up the forecast.io platform.""" uri = (r'https://api.(darksky.net|forecast.io)\/forecast\/(\w+)\/' r'(-?\d+\.?\d*),(-?\d+\.?\d*)') - mock_req.get(re.compile(uri), - text=load_fixture('darksky.json')) - darksky.setup_platform(self.hass, self.config, self.add_entities) + mock_req.get(re.compile(uri), text=load_fixture('darksky.json')) + + assert setup_component(self.hass, 'sensor', VALID_CONFIG_MINIMAL) + self.assertTrue(mock_get_forecast.called) self.assertEqual(mock_get_forecast.call_count, 1) - self.assertEqual(len(self.entities), 7) + self.assertEqual(len(self.hass.states.entity_ids()), 7) + + state = self.hass.states.get('sensor.dark_sky_summary') + assert state is not None + self.assertEqual(state.state, 'Clear') + self.assertEqual(state.attributes.get('friendly_name'), + 'Dark Sky Summary') From 98620d8ce848ded4de5d67e90d8333c0782a25bd Mon Sep 17 00:00:00 2001 From: Jeroen ter Heerdt Date: Thu, 22 Mar 2018 18:53:52 +0100 Subject: [PATCH 167/220] Fixing Egardia 'home armed' state not shown correctly. (#13335) * Fixing Egardia 'home armed' state not shown correctly. * Updating requirements_all. * Adding DEPEDENCY list to Egardia components. * updating requirements_all --- homeassistant/components/alarm_control_panel/egardia.py | 7 +++++-- homeassistant/components/binary_sensor/egardia.py | 2 +- homeassistant/components/egardia.py | 2 +- requirements_all.txt | 3 +-- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/alarm_control_panel/egardia.py b/homeassistant/components/alarm_control_panel/egardia.py index 845eb81bbe0..f0db378ec15 100644 --- a/homeassistant/components/alarm_control_panel/egardia.py +++ b/homeassistant/components/alarm_control_panel/egardia.py @@ -12,13 +12,14 @@ import requests import homeassistant.components.alarm_control_panel as alarm from homeassistant.const import ( STATE_ALARM_DISARMED, STATE_ALARM_ARMED_HOME, - STATE_ALARM_ARMED_AWAY, STATE_ALARM_TRIGGERED) + STATE_ALARM_ARMED_AWAY, STATE_ALARM_TRIGGERED, + STATE_ALARM_ARMED_NIGHT) from homeassistant.components.egardia import ( EGARDIA_DEVICE, EGARDIA_SERVER, REPORT_SERVER_CODES_IGNORE, CONF_REPORT_SERVER_CODES, CONF_REPORT_SERVER_ENABLED, CONF_REPORT_SERVER_PORT ) -REQUIREMENTS = ['pythonegardia==1.0.38'] +DEPENDENCIES = ['egardia'] _LOGGER = logging.getLogger(__name__) @@ -27,6 +28,8 @@ STATES = { 'DAY HOME': STATE_ALARM_ARMED_HOME, 'DISARM': STATE_ALARM_DISARMED, 'ARMHOME': STATE_ALARM_ARMED_HOME, + 'HOME': STATE_ALARM_ARMED_HOME, + 'NIGHT HOME': STATE_ALARM_ARMED_NIGHT, 'TRIGGERED': STATE_ALARM_TRIGGERED } diff --git a/homeassistant/components/binary_sensor/egardia.py b/homeassistant/components/binary_sensor/egardia.py index ab88de9d3c9..76d90e78376 100644 --- a/homeassistant/components/binary_sensor/egardia.py +++ b/homeassistant/components/binary_sensor/egardia.py @@ -12,7 +12,7 @@ from homeassistant.const import STATE_ON, STATE_OFF from homeassistant.components.egardia import ( EGARDIA_DEVICE, ATTR_DISCOVER_DEVICES) _LOGGER = logging.getLogger(__name__) - +DEPENDENCIES = ['egardia'] EGARDIA_TYPE_TO_DEVICE_CLASS = {'IR Sensor': 'motion', 'Door Contact': 'opening', 'IR': 'motion'} diff --git a/homeassistant/components/egardia.py b/homeassistant/components/egardia.py index 2cfc44a407b..f350ea56bb4 100644 --- a/homeassistant/components/egardia.py +++ b/homeassistant/components/egardia.py @@ -15,7 +15,7 @@ from homeassistant.const import ( CONF_PORT, CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_NAME, EVENT_HOMEASSISTANT_STOP) -REQUIREMENTS = ['pythonegardia==1.0.38'] +REQUIREMENTS = ['pythonegardia==1.0.39'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 793403f26d7..837770d7c11 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1006,8 +1006,7 @@ python_opendata_transport==0.0.3 python_openzwave==0.4.3 # homeassistant.components.egardia -# homeassistant.components.alarm_control_panel.egardia -pythonegardia==1.0.38 +pythonegardia==1.0.39 # homeassistant.components.sensor.whois pythonwhois==2.4.3 From fb1fafefabe151421f7f4b94835b05936e2e577c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 22 Mar 2018 12:21:33 -0700 Subject: [PATCH 168/220] Include all config flow translations with backend translations (#13394) --- homeassistant/helpers/translation.py | 3 ++- tests/helpers/test_translation.py | 28 +++++++++++++++++++++++++++- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/homeassistant/helpers/translation.py b/homeassistant/helpers/translation.py index 9d1773de4d2..26cb34ede8c 100644 --- a/homeassistant/helpers/translation.py +++ b/homeassistant/helpers/translation.py @@ -4,6 +4,7 @@ import logging from typing import Optional # NOQA from os import path +from homeassistant import config_entries from homeassistant.loader import get_component, bind_hass from homeassistant.util.json import load_json @@ -89,7 +90,7 @@ async def async_get_component_resources(hass, language): translation_cache = hass.data[TRANSLATION_STRING_CACHE][language] # Get the set of components - components = hass.config.components + components = hass.config.components | set(config_entries.FLOWS) # Calculate the missing components missing_components = components - set(translation_cache) diff --git a/tests/helpers/test_translation.py b/tests/helpers/test_translation.py index 840f665f410..c72efca8c29 100644 --- a/tests/helpers/test_translation.py +++ b/tests/helpers/test_translation.py @@ -1,11 +1,23 @@ """Test the translation helper.""" # pylint: disable=protected-access from os import path +from unittest.mock import patch +import pytest + +from homeassistant import config_entries import homeassistant.helpers.translation as translation from homeassistant.setup import async_setup_component +@pytest.fixture +def mock_config_flows(): + """Mock the config flows.""" + flows = [] + with patch.object(config_entries, 'FLOWS', flows): + yield flows + + def test_flatten(): """Test the flatten function.""" data = { @@ -71,7 +83,7 @@ def test_load_translations_files(hass): } -async def test_get_translations(hass): +async def test_get_translations(hass, mock_config_flows): """Test the get translations helper.""" translations = await translation.async_get_translations(hass, 'en') assert translations == {} @@ -106,3 +118,17 @@ async def test_get_translations(hass): 'component.switch.state.string1': 'Value 1', 'component.switch.state.string2': 'Value 2', } + + +async def test_get_translations_loads_config_flows(hass, mock_config_flows): + """Test the get translations helper loads config flow translations.""" + mock_config_flows.append('component1') + + with patch.object(translation, 'component_translation_file', + return_value='bla.json'), \ + patch.object(translation, 'load_translations_files', return_value={ + 'component1': {'hello': 'world'}}): + translations = await translation.async_get_translations(hass, 'en') + assert translations == { + 'component.component1.hello': 'world' + } From c50b00226c221c395143d2a92c8696eacdf60e01 Mon Sep 17 00:00:00 2001 From: Gerard Date: Fri, 23 Mar 2018 07:32:33 +0100 Subject: [PATCH 169/220] Avoid breaking change for BMW ConnectedDrive sensors in #12591 (#13380) --- .../binary_sensor/bmw_connected_drive.py | 15 ++++++++------- .../components/lock/bmw_connected_drive.py | 3 ++- .../components/sensor/bmw_connected_drive.py | 3 ++- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/binary_sensor/bmw_connected_drive.py b/homeassistant/components/binary_sensor/bmw_connected_drive.py index 0c848a57fbf..0f3edd86dcd 100644 --- a/homeassistant/components/binary_sensor/bmw_connected_drive.py +++ b/homeassistant/components/binary_sensor/bmw_connected_drive.py @@ -15,8 +15,8 @@ DEPENDENCIES = ['bmw_connected_drive'] _LOGGER = logging.getLogger(__name__) SENSOR_TYPES = { - 'all_lids_closed': ['Doors', 'opening'], - 'all_windows_closed': ['Windows', 'opening'], + 'lids': ['Doors', 'opening'], + 'windows': ['Windows', 'opening'], 'door_lock_state': ['Door lock state', 'safety'] } @@ -45,7 +45,8 @@ class BMWConnectedDriveSensor(BinarySensorDevice): self._account = account self._vehicle = vehicle self._attribute = attribute - self._name = sensor_name + self._name = '{} {}'.format(self._vehicle.modelName, self._attribute) + self._sensor_name = sensor_name self._device_class = device_class self._state = None @@ -77,10 +78,10 @@ class BMWConnectedDriveSensor(BinarySensorDevice): 'car': self._vehicle.modelName } - if self._attribute == 'all_lids_closed': + if self._attribute == 'lids': for lid in vehicle_state.lids: result[lid.name] = lid.state.value - elif self._attribute == 'all_windows_closed': + elif self._attribute == 'windows': for window in vehicle_state.windows: result[window.name] = window.state.value elif self._attribute == 'door_lock_state': @@ -93,10 +94,10 @@ class BMWConnectedDriveSensor(BinarySensorDevice): vehicle_state = self._vehicle.state # device class opening: On means open, Off means closed - if self._attribute == 'all_lids_closed': + if self._attribute == 'lids': _LOGGER.debug("Status of lid: %s", vehicle_state.all_lids_closed) self._state = not vehicle_state.all_lids_closed - if self._attribute == 'all_windows_closed': + if self._attribute == 'windows': self._state = not vehicle_state.all_windows_closed # device class safety: On means unsafe, Off means safe if self._attribute == 'door_lock_state': diff --git a/homeassistant/components/lock/bmw_connected_drive.py b/homeassistant/components/lock/bmw_connected_drive.py index 4592fd7cae9..c500e02b2f7 100644 --- a/homeassistant/components/lock/bmw_connected_drive.py +++ b/homeassistant/components/lock/bmw_connected_drive.py @@ -37,7 +37,8 @@ class BMWLock(LockDevice): self._account = account self._vehicle = vehicle self._attribute = attribute - self._name = sensor_name + self._name = '{} {}'.format(self._vehicle.modelName, self._attribute) + self._sensor_name = sensor_name self._state = None @property diff --git a/homeassistant/components/sensor/bmw_connected_drive.py b/homeassistant/components/sensor/bmw_connected_drive.py index 76719763931..3208c7377df 100644 --- a/homeassistant/components/sensor/bmw_connected_drive.py +++ b/homeassistant/components/sensor/bmw_connected_drive.py @@ -51,7 +51,8 @@ class BMWConnectedDriveSensor(Entity): self._attribute = attribute self._state = None self._unit_of_measurement = None - self._name = sensor_name + self._name = '{} {}'.format(self._vehicle.modelName, self._attribute) + self._sensor_name = sensor_name self._icon = icon @property From 2c7bc6eaf8953b222c4f4b99eaf36bd58349b05e Mon Sep 17 00:00:00 2001 From: Erik Eriksson Date: Fri, 23 Mar 2018 11:30:44 +0100 Subject: [PATCH 170/220] Support setting icon when configuring MQTT entity (#13304) --- homeassistant/components/sensor/mqtt.py | 12 ++++++++++-- homeassistant/components/switch/mqtt.py | 13 +++++++++++-- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/sensor/mqtt.py b/homeassistant/components/sensor/mqtt.py index b19f5721e4f..d191b9a22e8 100644 --- a/homeassistant/components/sensor/mqtt.py +++ b/homeassistant/components/sensor/mqtt.py @@ -17,7 +17,7 @@ from homeassistant.components.mqtt import ( CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, MqttAvailability) from homeassistant.const import ( CONF_FORCE_UPDATE, CONF_NAME, CONF_VALUE_TEMPLATE, STATE_UNKNOWN, - CONF_UNIT_OF_MEASUREMENT) + CONF_UNIT_OF_MEASUREMENT, CONF_ICON) from homeassistant.helpers.entity import Entity import homeassistant.components.mqtt as mqtt import homeassistant.helpers.config_validation as cv @@ -36,6 +36,7 @@ DEPENDENCIES = ['mqtt'] PLATFORM_SCHEMA = mqtt.MQTT_RO_PLATFORM_SCHEMA.extend({ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, + vol.Optional(CONF_ICON): cv.icon, vol.Optional(CONF_JSON_ATTRS, default=[]): cv.ensure_list_csv, vol.Optional(CONF_EXPIRE_AFTER): cv.positive_int, vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE): cv.boolean, @@ -59,6 +60,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): config.get(CONF_UNIT_OF_MEASUREMENT), config.get(CONF_FORCE_UPDATE), config.get(CONF_EXPIRE_AFTER), + config.get(CONF_ICON), value_template, config.get(CONF_JSON_ATTRS), config.get(CONF_AVAILABILITY_TOPIC), @@ -71,7 +73,7 @@ class MqttSensor(MqttAvailability, Entity): """Representation of a sensor that can be updated using MQTT.""" def __init__(self, name, state_topic, qos, unit_of_measurement, - force_update, expire_after, value_template, + force_update, expire_after, icon, value_template, json_attributes, availability_topic, payload_available, payload_not_available): """Initialize the sensor.""" @@ -85,6 +87,7 @@ class MqttSensor(MqttAvailability, Entity): self._force_update = force_update self._template = value_template self._expire_after = expire_after + self._icon = icon self._expiration_trigger = None self._json_attributes = set(json_attributes) self._attributes = None @@ -170,3 +173,8 @@ class MqttSensor(MqttAvailability, Entity): def device_state_attributes(self): """Return the state attributes.""" return self._attributes + + @property + def icon(self): + """Return the icon.""" + return self._icon diff --git a/homeassistant/components/switch/mqtt.py b/homeassistant/components/switch/mqtt.py index a4aea1ded9f..f3bd0bef012 100644 --- a/homeassistant/components/switch/mqtt.py +++ b/homeassistant/components/switch/mqtt.py @@ -17,7 +17,7 @@ from homeassistant.components.mqtt import ( from homeassistant.components.switch import SwitchDevice from homeassistant.const import ( CONF_NAME, CONF_OPTIMISTIC, CONF_VALUE_TEMPLATE, CONF_PAYLOAD_OFF, - CONF_PAYLOAD_ON) + CONF_PAYLOAD_ON, CONF_ICON) import homeassistant.components.mqtt as mqtt import homeassistant.helpers.config_validation as cv @@ -32,6 +32,7 @@ DEFAULT_OPTIMISTIC = False PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_ICON): cv.icon, vol.Optional(CONF_PAYLOAD_ON, default=DEFAULT_PAYLOAD_ON): cv.string, vol.Optional(CONF_PAYLOAD_OFF, default=DEFAULT_PAYLOAD_OFF): cv.string, vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, @@ -50,6 +51,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): async_add_devices([MqttSwitch( config.get(CONF_NAME), + config.get(CONF_ICON), config.get(CONF_STATE_TOPIC), config.get(CONF_COMMAND_TOPIC), config.get(CONF_AVAILABILITY_TOPIC), @@ -67,7 +69,8 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): class MqttSwitch(MqttAvailability, SwitchDevice): """Representation of a switch that can be toggled using MQTT.""" - def __init__(self, name, state_topic, command_topic, availability_topic, + def __init__(self, name, icon, + state_topic, command_topic, availability_topic, qos, retain, payload_on, payload_off, optimistic, payload_available, payload_not_available, value_template): """Initialize the MQTT switch.""" @@ -75,6 +78,7 @@ class MqttSwitch(MqttAvailability, SwitchDevice): payload_not_available) self._state = False self._name = name + self._icon = icon self._state_topic = state_topic self._command_topic = command_topic self._qos = qos @@ -130,6 +134,11 @@ class MqttSwitch(MqttAvailability, SwitchDevice): """Return true if we do optimistic updates.""" return self._optimistic + @property + def icon(self): + """Return the icon.""" + return self._icon + @asyncio.coroutine def async_turn_on(self, **kwargs): """Turn the device on. From ba7178dc0cc792707463d1e1ceb17fcf1aff69e7 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Fri, 23 Mar 2018 18:06:07 +0100 Subject: [PATCH 171/220] Enhance mysensors sensor units and icons (#13365) --- homeassistant/components/sensor/mysensors.py | 83 +++++++++++--------- 1 file changed, 46 insertions(+), 37 deletions(-) diff --git a/homeassistant/components/sensor/mysensors.py b/homeassistant/components/sensor/mysensors.py index a8daf212e57..3876b260dfc 100644 --- a/homeassistant/components/sensor/mysensors.py +++ b/homeassistant/components/sensor/mysensors.py @@ -8,6 +8,31 @@ from homeassistant.components import mysensors from homeassistant.components.sensor import DOMAIN from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT +SENSORS = { + 'V_TEMP': [None, 'mdi:thermometer'], + 'V_HUM': ['%', 'mdi:water-percent'], + 'V_DIMMER': ['%', 'mdi:percent'], + 'V_LIGHT_LEVEL': ['%', 'white-balance-sunny'], + 'V_DIRECTION': ['°', 'mdi:compass'], + 'V_WEIGHT': ['kg', 'mdi:weight-kilogram'], + 'V_DISTANCE': ['m', 'mdi:ruler'], + 'V_IMPEDANCE': ['ohm', None], + 'V_WATT': ['W', None], + 'V_KWH': ['kWh', None], + 'V_FLOW': ['m', None], + 'V_VOLUME': ['m³', None], + 'V_VOLTAGE': ['V', 'mdi:flash'], + 'V_CURRENT': ['A', 'mdi:flash-auto'], + 'V_PERCENTAGE': ['%', 'mdi:percent'], + 'V_LEVEL': { + 'S_SOUND': ['dB', 'mdi:volume-high'], 'S_VIBRATION': ['Hz', None], + 'S_LIGHT_LEVEL': ['lux', 'white-balance-sunny']}, + 'V_ORP': ['mV', None], + 'V_EC': ['μS/cm', None], + 'V_VAR': ['var', None], + 'V_VA': ['VA', None], +} + def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the MySensors platform for sensors.""" @@ -32,45 +57,29 @@ class MySensorsSensor(mysensors.MySensorsEntity): """Return the state of the device.""" return self._values.get(self.value_type) + @property + def icon(self): + """Return the icon to use in the frontend, if any.""" + _, icon = self._get_sensor_type() + return icon + @property def unit_of_measurement(self): """Return the unit of measurement of this entity.""" + set_req = self.gateway.const.SetReq + if (float(self.gateway.protocol_version) >= 1.5 and + set_req.V_UNIT_PREFIX in self._values): + return self._values[set_req.V_UNIT_PREFIX] + unit, _ = self._get_sensor_type() + return unit + + def _get_sensor_type(self): + """Return list with unit and icon of sensor type.""" pres = self.gateway.const.Presentation set_req = self.gateway.const.SetReq - unit_map = { - set_req.V_TEMP: (TEMP_CELSIUS - if self.gateway.metric else TEMP_FAHRENHEIT), - set_req.V_HUM: '%', - set_req.V_DIMMER: '%', - set_req.V_LIGHT_LEVEL: '%', - set_req.V_DIRECTION: '°', - set_req.V_WEIGHT: 'kg', - set_req.V_DISTANCE: 'm', - set_req.V_IMPEDANCE: 'ohm', - set_req.V_WATT: 'W', - set_req.V_KWH: 'kWh', - set_req.V_FLOW: 'm', - set_req.V_VOLUME: 'm³', - set_req.V_VOLTAGE: 'V', - set_req.V_CURRENT: 'A', - } - if float(self.gateway.protocol_version) >= 1.5: - if set_req.V_UNIT_PREFIX in self._values: - return self._values[ - set_req.V_UNIT_PREFIX] - unit_map.update({ - set_req.V_PERCENTAGE: '%', - set_req.V_LEVEL: { - pres.S_SOUND: 'dB', pres.S_VIBRATION: 'Hz', - pres.S_LIGHT_LEVEL: 'lux'}}) - if float(self.gateway.protocol_version) >= 2.0: - unit_map.update({ - set_req.V_ORP: 'mV', - set_req.V_EC: 'μS/cm', - set_req.V_VAR: 'var', - set_req.V_VA: 'VA', - }) - unit = unit_map.get(self.value_type) - if isinstance(unit, dict): - unit = unit.get(self.child_type) - return unit + SENSORS[set_req.V_TEMP.name][0] = ( + TEMP_CELSIUS if self.gateway.metric else TEMP_FAHRENHEIT) + sensor_type = SENSORS.get(set_req(self.value_type).name, [None, None]) + if isinstance(sensor_type, dict): + sensor_type = sensor_type.get(pres(self.child_type).name) + return sensor_type From 79c9d3ba102dcc23f32504e2f0d1774f44e9bf81 Mon Sep 17 00:00:00 2001 From: cgtobi Date: Fri, 23 Mar 2018 18:08:16 +0100 Subject: [PATCH 172/220] Fix incorrect unit of measurement for precip_intensity. (#13415) --- homeassistant/components/sensor/darksky.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/darksky.py b/homeassistant/components/sensor/darksky.py index 261e0a62409..7d535c5f1d9 100644 --- a/homeassistant/components/sensor/darksky.py +++ b/homeassistant/components/sensor/darksky.py @@ -54,7 +54,8 @@ SENSOR_TYPES = { 'mdi:weather-pouring', ['currently', 'minutely', 'hourly', 'daily']], 'precip_intensity': ['Precip Intensity', - 'mm', 'in', 'mm', 'mm', 'mm', 'mdi:weather-rainy', + 'mm/h', 'in', 'mm/h', 'mm/h', 'mm/h', + 'mdi:weather-rainy', ['currently', 'minutely', 'hourly', 'daily']], 'precip_probability': ['Precip Probability', '%', '%', '%', '%', '%', 'mdi:water-percent', @@ -100,7 +101,8 @@ SENSOR_TYPES = { '°C', '°F', '°C', '°C', '°C', 'mdi:thermometer', ['currently', 'hourly', 'daily']], 'precip_intensity_max': ['Daily Max Precip Intensity', - 'mm', 'in', 'mm', 'mm', 'mm', 'mdi:thermometer', + 'mm/h', 'in', 'mm/h', 'mm/h', 'mm/h', + 'mdi:thermometer', ['currently', 'hourly', 'daily']], 'uv_index': ['UV Index', UNIT_UV_INDEX, UNIT_UV_INDEX, UNIT_UV_INDEX, From 5ec6f25d4e1c179931c5b171702a9231eef34bfe Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Fri, 23 Mar 2018 18:09:18 +0100 Subject: [PATCH 173/220] Fix Sonos playing Sveriges Radio (#13401) --- homeassistant/components/media_player/sonos.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/media_player/sonos.py b/homeassistant/components/media_player/sonos.py index 34ef146fc05..b10c761d532 100644 --- a/homeassistant/components/media_player/sonos.py +++ b/homeassistant/components/media_player/sonos.py @@ -321,7 +321,7 @@ def _is_radio_uri(uri): """Return whether the URI is a radio stream.""" radio_schemes = ( 'x-rincon-mp3radio:', 'x-sonosapi-stream:', 'x-sonosapi-radio:', - 'hls-radio:') + 'x-sonosapi-hls:', 'hls-radio:') return uri.startswith(radio_schemes) From 23f06b0040649e41dc4ff3fcbf509455b90fa7e8 Mon Sep 17 00:00:00 2001 From: Philip Rosenberg-Watt Date: Fri, 23 Mar 2018 11:10:52 -0600 Subject: [PATCH 174/220] Cache LaMetric devices for offline use (#13379) If the connection to the LaMetric server fails, we should still be able to send notifications to known and reachable devices. --- homeassistant/components/notify/lametric.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/notify/lametric.py b/homeassistant/components/notify/lametric.py index f4c9c391408..895ffd9db10 100644 --- a/homeassistant/components/notify/lametric.py +++ b/homeassistant/components/notify/lametric.py @@ -6,6 +6,7 @@ https://home-assistant.io/components/notify.lametric/ """ import logging +from requests.exceptions import ConnectionError as RequestsConnectionError import voluptuous as vol from homeassistant.components.notify import ( @@ -49,6 +50,7 @@ class LaMetricNotificationService(BaseNotificationService): self._icon = icon self._lifetime = lifetime self._cycles = cycles + self._devices = [] # pylint: disable=broad-except def send_message(self, message="", **kwargs): @@ -86,12 +88,15 @@ class LaMetricNotificationService(BaseNotificationService): model = Model(frames=frames, cycles=cycles, sound=sound) lmn = self.hasslametricmanager.manager try: - devices = lmn.get_devices() + self._devices = lmn.get_devices() except TokenExpiredError: _LOGGER.debug("Token expired, fetching new token") lmn.get_token() - devices = lmn.get_devices() - for dev in devices: + self._devices = lmn.get_devices() + except RequestsConnectionError: + _LOGGER.warning("Problem connecting to LaMetric, " + "using cached devices instead") + for dev in self._devices: if targets is None or dev["name"] in targets: try: lmn.set_device(dev) From 23165cbd1ac8ba1528649c04b56d598664e1da8b Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Fri, 23 Mar 2018 18:11:53 +0100 Subject: [PATCH 175/220] Enhance mysensors binary sensor device classes (#13367) --- .../components/binary_sensor/mysensors.py | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/binary_sensor/mysensors.py b/homeassistant/components/binary_sensor/mysensors.py index 19fa02f63df..1e9359b6902 100644 --- a/homeassistant/components/binary_sensor/mysensors.py +++ b/homeassistant/components/binary_sensor/mysensors.py @@ -9,6 +9,17 @@ from homeassistant.components.binary_sensor import ( DEVICE_CLASSES, DOMAIN, BinarySensorDevice) from homeassistant.const import STATE_ON +SENSORS = { + 'S_DOOR': 'door', + 'S_MOTION': 'motion', + 'S_SMOKE': 'smoke', + 'S_SPRINKLER': 'safety', + 'S_WATER_LEAK': 'safety', + 'S_SOUND': 'sound', + 'S_VIBRATION': 'vibration', + 'S_MOISTURE': 'moisture', +} + def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the MySensors platform for binary sensors.""" @@ -29,18 +40,7 @@ class MySensorsBinarySensor(mysensors.MySensorsEntity, BinarySensorDevice): def device_class(self): """Return the class of this sensor, from DEVICE_CLASSES.""" pres = self.gateway.const.Presentation - class_map = { - pres.S_DOOR: 'opening', - pres.S_MOTION: 'motion', - pres.S_SMOKE: 'smoke', - } - if float(self.gateway.protocol_version) >= 1.5: - class_map.update({ - pres.S_SPRINKLER: 'sprinkler', - pres.S_WATER_LEAK: 'leak', - pres.S_SOUND: 'sound', - pres.S_VIBRATION: 'vibration', - pres.S_MOISTURE: 'moisture', - }) - if class_map.get(self.child_type) in DEVICE_CLASSES: - return class_map.get(self.child_type) + device_class = SENSORS.get(pres(self.child_type).name) + if device_class in DEVICE_CLASSES: + return device_class + return None From 8852e526010ef549ed288209056034092870020f Mon Sep 17 00:00:00 2001 From: Jerad Meisner Date: Fri, 23 Mar 2018 10:22:01 -0700 Subject: [PATCH 176/220] Switched to async/await. Bumped pyxeoma version (#13404) --- homeassistant/components/camera/xeoma.py | 16 +++++++--------- requirements_all.txt | 2 +- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/camera/xeoma.py b/homeassistant/components/camera/xeoma.py index 5836a9c94dc..cec04b52047 100644 --- a/homeassistant/components/camera/xeoma.py +++ b/homeassistant/components/camera/xeoma.py @@ -4,7 +4,6 @@ Support for Xeoma Cameras. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/camera.xeoma/ """ -import asyncio import logging import voluptuous as vol @@ -14,7 +13,7 @@ from homeassistant.const import ( CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME) from homeassistant.helpers import config_validation as cv -REQUIREMENTS = ['pyxeoma==1.3'] +REQUIREMENTS = ['pyxeoma==1.4.0'] _LOGGER = logging.getLogger(__name__) @@ -41,8 +40,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_devices, discovery_info=None): +async def async_setup_platform(hass, config, async_add_devices, + discovery_info=None): """Discover and setup Xeoma Cameras.""" from pyxeoma.xeoma import Xeoma, XeomaError @@ -53,8 +52,8 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): xeoma = Xeoma(host, login, password) try: - yield from xeoma.async_test_connection() - discovered_image_names = yield from xeoma.async_get_image_names() + await xeoma.async_test_connection() + discovered_image_names = await xeoma.async_get_image_names() discovered_cameras = [ { CONF_IMAGE_NAME: image_name, @@ -103,12 +102,11 @@ class XeomaCamera(Camera): self._password = password self._last_image = None - @asyncio.coroutine - def async_camera_image(self): + async def async_camera_image(self): """Return a still image response from the camera.""" from pyxeoma.xeoma import XeomaError try: - image = yield from self._xeoma.async_get_camera_image( + image = await self._xeoma.async_get_camera_image( self._image, self._username, self._password) self._last_image = image except XeomaError as err: diff --git a/requirements_all.txt b/requirements_all.txt index 837770d7c11..75fd6de8f46 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1048,7 +1048,7 @@ pywebpush==1.6.0 pywemo==0.4.25 # homeassistant.components.camera.xeoma -pyxeoma==1.3 +pyxeoma==1.4.0 # homeassistant.components.zabbix pyzabbix==0.7.4 From 553920780f99c56dcbd3ee7dd7f61b48c0d81e18 Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Fri, 23 Mar 2018 18:22:48 +0100 Subject: [PATCH 177/220] Added default return value for HS_Color (#13395) --- homeassistant/components/homekit/type_lights.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/homekit/type_lights.py b/homeassistant/components/homekit/type_lights.py index 6cd60698110..c723fcc08a6 100644 --- a/homeassistant/components/homekit/type_lights.py +++ b/homeassistant/components/homekit/type_lights.py @@ -134,7 +134,8 @@ class Light(HomeAccessory): # Handle Color if CHAR_SATURATION in self.chars and CHAR_HUE in self.chars: - hue, saturation = new_state.attributes.get(ATTR_HS_COLOR) + hue, saturation = new_state.attributes.get( + ATTR_HS_COLOR, (None, None)) if not self._flag[RGB_COLOR] and ( hue != self._hue or saturation != self._saturation): self.char_hue.set_value(hue, should_callback=False) From 2497dd5e33d227e24d152360e39fefa926a167ae Mon Sep 17 00:00:00 2001 From: Adam Mills Date: Fri, 23 Mar 2018 14:01:40 -0400 Subject: [PATCH 178/220] Hue: Use the currently active color mode (#13376) * Hue: Use the currently active color mode * Round hue/sat colors before reporting to API * .gitignore cache fix --- .gitignore | 1 + homeassistant/components/light/__init__.py | 4 +++ homeassistant/components/light/hue.py | 15 +++++++++ tests/components/light/test_hue.py | 37 +++++++++++++++++++++- 4 files changed, 56 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 33a1f4f9a4b..bf49a1b61c1 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,7 @@ Icon # pytest .pytest_cache +.cache # GITHUB Proposed Python stuff: *.py[cod] diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index f03521947b7..eea6c821fc0 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -501,6 +501,10 @@ class Light(ToggleEntity): *data[ATTR_HS_COLOR]) data[ATTR_XY_COLOR] = color_util.color_hs_to_xy( *data[ATTR_HS_COLOR]) + data[ATTR_HS_COLOR] = ( + round(data[ATTR_HS_COLOR][0], 3), + round(data[ATTR_HS_COLOR][1], 3), + ) return data diff --git a/homeassistant/components/light/hue.py b/homeassistant/components/light/hue.py index b1562aaba8f..71e3d4fa30b 100644 --- a/homeassistant/components/light/hue.py +++ b/homeassistant/components/light/hue.py @@ -225,9 +225,20 @@ class HueLight(Light): return self.light.action.get('bri') return self.light.state.get('bri') + @property + def _color_mode(self): + """Return the hue color mode.""" + if self.is_group: + return self.light.action.get('colormode') + return self.light.state.get('colormode') + @property def hs_color(self): """Return the hs color value.""" + # Don't return hue/sat if in color temperature mode + if self._color_mode == "ct": + return None + if self.is_group: return ( self.light.action.get('hue') / 65535 * 360, @@ -241,6 +252,10 @@ class HueLight(Light): @property def color_temp(self): """Return the CT color value.""" + # Don't return color temperature unless in color temperature mode + if self._color_mode != "ct": + return None + if self.is_group: return self.light.action.get('ct') return self.light.state.get('ct') diff --git a/tests/components/light/test_hue.py b/tests/components/light/test_hue.py index 8abf51fdf0c..54bb2184a64 100644 --- a/tests/components/light/test_hue.py +++ b/tests/components/light/test_hue.py @@ -227,13 +227,48 @@ async def test_lights(hass, mock_bridge): assert lamp_1 is not None assert lamp_1.state == 'on' assert lamp_1.attributes['brightness'] == 144 - assert lamp_1.attributes['color_temp'] == 467 + assert lamp_1.attributes['hs_color'] == (71.896, 83.137) lamp_2 = hass.states.get('light.hue_lamp_2') assert lamp_2 is not None assert lamp_2.state == 'off' +async def test_lights_color_mode(hass, mock_bridge): + """Test that lights only report appropriate color mode.""" + mock_bridge.mock_light_responses.append(LIGHT_RESPONSE) + await setup_bridge(hass, mock_bridge) + + lamp_1 = hass.states.get('light.hue_lamp_1') + assert lamp_1 is not None + assert lamp_1.state == 'on' + assert lamp_1.attributes['brightness'] == 144 + assert lamp_1.attributes['hs_color'] == (71.896, 83.137) + assert 'color_temp' not in lamp_1.attributes + + new_light1_on = LIGHT_1_ON.copy() + new_light1_on['state'] = new_light1_on['state'].copy() + new_light1_on['state']['colormode'] = 'ct' + mock_bridge.mock_light_responses.append({ + "1": new_light1_on, + }) + mock_bridge.mock_group_responses.append({}) + + # Calling a service will trigger the updates to run + await hass.services.async_call('light', 'turn_on', { + 'entity_id': 'light.hue_lamp_2' + }, blocking=True) + # 2x light update, 1 turn on request + assert len(mock_bridge.mock_requests) == 3 + + lamp_1 = hass.states.get('light.hue_lamp_1') + assert lamp_1 is not None + assert lamp_1.state == 'on' + assert lamp_1.attributes['brightness'] == 144 + assert lamp_1.attributes['color_temp'] == 467 + assert 'hs_color' not in lamp_1.attributes + + async def test_groups(hass, mock_bridge): """Test the update_lights function with some lights.""" mock_bridge.allow_groups = True From df8596e896af9c4858d3eb477bf5119bbe2e2086 Mon Sep 17 00:00:00 2001 From: Mattias Welponer Date: Fri, 23 Mar 2018 19:05:02 +0100 Subject: [PATCH 179/220] Cleanup homematicip_cloud (#13356) * Cleanup and proposed changes from MartinHjelmare * Removed coroutine decorator from async_added_to_hass * Added blank line * Fix of component url * Fix of component url * Fix url of the sensor component --- homeassistant/components/homematicip_cloud.py | 16 ++++-- .../components/sensor/homematicip_cloud.py | 57 ++++++++++--------- 2 files changed, 40 insertions(+), 33 deletions(-) diff --git a/homeassistant/components/homematicip_cloud.py b/homeassistant/components/homematicip_cloud.py index a89678624eb..180d6943d8a 100644 --- a/homeassistant/components/homematicip_cloud.py +++ b/homeassistant/components/homematicip_cloud.py @@ -2,13 +2,14 @@ Support for HomematicIP components. For more details about this component, please refer to the documentation at -https://home-assistant.io/components/homematicip/ +https://home-assistant.io/components/homematicip_cloud/ """ import logging from socket import timeout import voluptuous as vol + from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import (dispatcher_send, @@ -49,12 +50,14 @@ ATTR_FIRMWARE_STATE = 'firmware_state' ATTR_LOW_BATTERY = 'low_battery' ATTR_SABOTAGE = 'sabotage' ATTR_RSSI = 'rssi' +ATTR_TYPE = 'type' def setup(hass, config): """Set up the HomematicIP component.""" # pylint: disable=import-error, no-name-in-module from homematicip.home import Home + hass.data.setdefault(DOMAIN, {}) homes = hass.data[DOMAIN] accesspoints = config.get(DOMAIN, []) @@ -100,19 +103,21 @@ def setup(hass, config): _LOGGER.info('HUB name: %s, id: %s', home.label, home.id) for component in ['sensor']: - load_platform(hass, component, DOMAIN, - {'homeid': home.id}, config) + load_platform(hass, component, DOMAIN, {'homeid': home.id}, config) + return True class HomematicipGenericDevice(Entity): """Representation of an HomematicIP generic device.""" - def __init__(self, hass, home, device, signal=None): + def __init__(self, home, device): """Initialize the generic device.""" - self.hass = hass self._home = home self._device = device + + async def async_added_to_hass(self): + """Register callbacks.""" async_dispatcher_connect( self.hass, EVENT_DEVICE_CHANGED, self._device_changed) @@ -162,6 +167,7 @@ class HomematicipGenericDevice(Entity): ATTR_FIRMWARE_STATE: self._device.updateState.lower(), ATTR_LOW_BATTERY: self._device.lowBat, ATTR_RSSI: self._device.rssiDeviceValue, + ATTR_TYPE: self._device.modelType } @property diff --git a/homeassistant/components/sensor/homematicip_cloud.py b/homeassistant/components/sensor/homematicip_cloud.py index 8f298bbb3f6..1a37aa1ad4e 100644 --- a/homeassistant/components/sensor/homematicip_cloud.py +++ b/homeassistant/components/sensor/homematicip_cloud.py @@ -2,14 +2,14 @@ Support for HomematicIP sensors. For more details about this component, please refer to the documentation at -https://home-assistant.io/components/homematicip/ +https://home-assistant.io/components/sensor.homematicip_cloud/ """ import logging from homeassistant.core import callback from homeassistant.helpers.entity import Entity -from homeassistant.helpers.dispatcher import dispatcher_connect +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.components.homematicip_cloud import ( HomematicipGenericDevice, DOMAIN, EVENT_HOME_CHANGED, ATTR_HOME_LABEL, ATTR_HOME_ID, ATTR_LOW_BATTERY, ATTR_RSSI) @@ -38,41 +38,43 @@ def setup_platform(hass, config, add_devices, discovery_info=None): HeatingThermostat, TemperatureHumiditySensorWithoutDisplay, TemperatureHumiditySensorDisplay) - _LOGGER.info('Setting up HomeMaticIP accespoint & generic devices') homeid = discovery_info['homeid'] home = hass.data[DOMAIN][homeid] - devices = [HomematicipAccesspoint(hass, home)] - if home.devices is None: - return + devices = [HomematicipAccesspoint(home)] + for device in home.devices: - devices.append(HomematicipDeviceStatus(hass, home, device)) + devices.append(HomematicipDeviceStatus(home, device)) if isinstance(device, HeatingThermostat): - devices.append(HomematicipHeatingThermostat(hass, home, device)) + devices.append(HomematicipHeatingThermostat(home, device)) if isinstance(device, TemperatureHumiditySensorWithoutDisplay): - devices.append(HomematicipSensorThermometer(hass, home, device)) - devices.append(HomematicipSensorHumidity(hass, home, device)) + devices.append(HomematicipSensorThermometer(home, device)) + devices.append(HomematicipSensorHumidity(home, device)) if isinstance(device, TemperatureHumiditySensorDisplay): - devices.append(HomematicipSensorThermometer(hass, home, device)) - devices.append(HomematicipSensorHumidity(hass, home, device)) - add_devices(devices) + devices.append(HomematicipSensorThermometer(home, device)) + devices.append(HomematicipSensorHumidity(home, device)) + + if home.devices: + add_devices(devices) class HomematicipAccesspoint(Entity): """Representation of an HomeMaticIP access point.""" - def __init__(self, hass, home): + def __init__(self, home): """Initialize the access point sensor.""" - self.hass = hass self._home = home - dispatcher_connect( - self.hass, EVENT_HOME_CHANGED, self._home_changed) _LOGGER.debug('Setting up access point %s', home.label) + async def async_added_to_hass(self): + """Register callbacks.""" + async_dispatcher_connect( + self.hass, EVENT_HOME_CHANGED, self._home_changed) + @callback def _home_changed(self, deviceid): """Handle device state changes.""" if deviceid is None or deviceid == self._home.id: - _LOGGER.debug('Event access point %s', self._home.label) + _LOGGER.debug('Event home %s', self._home.label) self.async_schedule_update_ha_state() @property @@ -109,9 +111,9 @@ class HomematicipAccesspoint(Entity): class HomematicipDeviceStatus(HomematicipGenericDevice): """Representation of an HomematicIP device status.""" - def __init__(self, hass, home, device, signal=None): + def __init__(self, home, device): """Initialize the device.""" - super().__init__(hass, home, device) + super().__init__(home, device) _LOGGER.debug('Setting up sensor device status: %s', device.label) @property @@ -147,9 +149,9 @@ class HomematicipDeviceStatus(HomematicipGenericDevice): class HomematicipHeatingThermostat(HomematicipGenericDevice): """MomematicIP heating thermostat representation.""" - def __init__(self, hass, home, device): + def __init__(self, home, device): """"Initialize heating thermostat.""" - super().__init__(hass, home, device) + super().__init__(home, device) _LOGGER.debug('Setting up heating thermostat device: %s', device.label) @property @@ -185,11 +187,10 @@ class HomematicipHeatingThermostat(HomematicipGenericDevice): class HomematicipSensorHumidity(HomematicipGenericDevice): """MomematicIP thermometer device.""" - def __init__(self, hass, home, device): + def __init__(self, home, device): """"Initialize the thermometer device.""" - super().__init__(hass, home, device) - _LOGGER.debug('Setting up humidity device: %s', - device.label) + super().__init__(home, device) + _LOGGER.debug('Setting up humidity device: %s', device.label) @property def name(self): @@ -223,9 +224,9 @@ class HomematicipSensorHumidity(HomematicipGenericDevice): class HomematicipSensorThermometer(HomematicipGenericDevice): """MomematicIP thermometer device.""" - def __init__(self, hass, home, device): + def __init__(self, home, device): """"Initialize the thermometer device.""" - super().__init__(hass, home, device) + super().__init__(home, device) _LOGGER.debug('Setting up thermometer device: %s', device.label) @property From 2532d67b9ae097b631b3950c85629f246e43f73c Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Fri, 23 Mar 2018 19:16:57 +0100 Subject: [PATCH 180/220] Add send sticker service to telegram bot (#13387) * Add send sticker service to telegram bot * A caption is not supported --- .../components/telegram_bot/__init__.py | 20 +++++++------ .../components/telegram_bot/services.yaml | 28 +++++++++++++++++++ 2 files changed, 39 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/telegram_bot/__init__.py b/homeassistant/components/telegram_bot/__init__.py index 3041e7b41e0..e43640e4df2 100644 --- a/homeassistant/components/telegram_bot/__init__.py +++ b/homeassistant/components/telegram_bot/__init__.py @@ -63,6 +63,7 @@ DOMAIN = 'telegram_bot' SERVICE_SEND_MESSAGE = 'send_message' SERVICE_SEND_PHOTO = 'send_photo' +SERVICE_SEND_STICKER = 'send_sticker' SERVICE_SEND_VIDEO = 'send_video' SERVICE_SEND_DOCUMENT = 'send_document' SERVICE_SEND_LOCATION = 'send_location' @@ -154,6 +155,7 @@ SERVICE_SCHEMA_DELETE_MESSAGE = vol.Schema({ SERVICE_MAP = { SERVICE_SEND_MESSAGE: SERVICE_SCHEMA_SEND_MESSAGE, SERVICE_SEND_PHOTO: SERVICE_SCHEMA_SEND_FILE, + SERVICE_SEND_STICKER: SERVICE_SCHEMA_SEND_FILE, SERVICE_SEND_VIDEO: SERVICE_SCHEMA_SEND_FILE, SERVICE_SEND_DOCUMENT: SERVICE_SCHEMA_SEND_FILE, SERVICE_SEND_LOCATION: SERVICE_SCHEMA_SEND_LOCATION, @@ -167,10 +169,10 @@ SERVICE_MAP = { def load_data(hass, url=None, filepath=None, username=None, password=None, authentication=None, num_retries=5): - """Load photo/document into ByteIO/File container from a source.""" + """Load data into ByteIO/File container from a source.""" try: if url is not None: - # Load photo from URL + # Load data from URL params = {"timeout": 15} if username is not None and password is not None: if authentication == HTTP_DIGEST_AUTHENTICATION: @@ -192,7 +194,7 @@ def load_data(hass, url=None, filepath=None, username=None, password=None, _LOGGER.warning("Empty data (retry #%s) in %s)", retry_num + 1, url) retry_num += 1 - _LOGGER.warning("Can't load photo in %s after %s retries", + _LOGGER.warning("Can't load data in %s after %s retries", url, retry_num) elif filepath is not None: if hass.config.is_allowed_path(filepath): @@ -200,10 +202,10 @@ def load_data(hass, url=None, filepath=None, username=None, password=None, _LOGGER.warning("'%s' are not secure to load data from!", filepath) else: - _LOGGER.warning("Can't load photo. No photo found in params!") + _LOGGER.warning("Can't load data. No data found in params!") except (OSError, TypeError) as error: - _LOGGER.error("Can't load photo into ByteIO: %s", error) + _LOGGER.error("Can't load data into ByteIO: %s", error) return None @@ -274,9 +276,8 @@ def async_setup(hass, config): if msgtype == SERVICE_SEND_MESSAGE: yield from hass.async_add_job( partial(notify_service.send_message, **kwargs)) - elif (msgtype == SERVICE_SEND_PHOTO or - msgtype == SERVICE_SEND_VIDEO or - msgtype == SERVICE_SEND_DOCUMENT): + elif msgtype in [SERVICE_SEND_PHOTO, SERVICE_SEND_STICKER, + SERVICE_SEND_VIDEO, SERVICE_SEND_DOCUMENT]: yield from hass.async_add_job( partial(notify_service.send_file, msgtype, **kwargs)) elif msgtype == SERVICE_SEND_LOCATION: @@ -524,11 +525,12 @@ class TelegramNotificationService: text=message, show_alert=show_alert, **params) def send_file(self, file_type=SERVICE_SEND_PHOTO, target=None, **kwargs): - """Send a photo, video, or document.""" + """Send a photo, sticker, video, or document.""" params = self._get_msg_kwargs(kwargs) caption = kwargs.get(ATTR_CAPTION) func_send = { SERVICE_SEND_PHOTO: self.bot.sendPhoto, + SERVICE_SEND_STICKER: self.bot.sendSticker, SERVICE_SEND_VIDEO: self.bot.sendVideo, SERVICE_SEND_DOCUMENT: self.bot.sendDocument }.get(file_type) diff --git a/homeassistant/components/telegram_bot/services.yaml b/homeassistant/components/telegram_bot/services.yaml index 4c144fe42db..d8039c0b384 100644 --- a/homeassistant/components/telegram_bot/services.yaml +++ b/homeassistant/components/telegram_bot/services.yaml @@ -59,6 +59,34 @@ send_photo: description: List of rows of commands, comma-separated, to make a custom inline keyboard with buttons with associated callback data. example: '["/button1, /button2", "/button3"] or [[["Text button1", "/button1"], ["Text button2", "/button2"]], [["Text button3", "/button3"]]]' +send_sticker: + description: Send a sticker. + fields: + url: + description: Remote path to an webp sticker. + example: 'http://example.org/path/to/the/sticker.webp' + file: + description: Local path to an webp sticker. + example: '/path/to/the/sticker.webp' + username: + description: Username for a URL which require HTTP basic authentication. + example: myuser + password: + description: Password for a URL which require HTTP basic authentication. + example: myuser_pwd + target: + description: An array of pre-authorized chat_ids to send the document to. If not present, first allowed chat_id is the default. + example: '[12345, 67890] or 12345' + disable_notification: + description: Sends the message silently. iOS users and Web users will not receive a notification, Android users will receive a notification with no sound. + example: true + keyboard: + description: List of rows of commands, comma-separated, to make a custom keyboard. + example: '["/command1, /command2", "/command3"]' + inline_keyboard: + description: List of rows of commands, comma-separated, to make a custom inline keyboard with buttons with associated callback data. + example: '["/button1, /button2", "/button3"] or [[["Text button1", "/button1"], ["Text button2", "/button2"]], [["Text button3", "/button3"]]]' + send_video: description: Send a video. fields: From 4bd6776443d6a56e20ac290d7b692878a2d49577 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 23 Mar 2018 12:13:52 -0700 Subject: [PATCH 181/220] Google assistant sync (#13392) * Add Google Assistant Sync API * Update const.py * Async/await --- homeassistant/components/cloud/__init__.py | 6 +- homeassistant/components/cloud/const.py | 4 +- homeassistant/components/cloud/http_api.py | 66 ++++++++++++++-------- tests/components/cloud/test_http_api.py | 19 +++++++ tests/components/cloud/test_init.py | 2 + 5 files changed, 71 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/cloud/__init__.py b/homeassistant/components/cloud/__init__.py index adf0b8f51b6..e73d043d366 100644 --- a/homeassistant/components/cloud/__init__.py +++ b/homeassistant/components/cloud/__init__.py @@ -37,6 +37,7 @@ CONF_FILTER = 'filter' CONF_GOOGLE_ACTIONS = 'google_actions' CONF_RELAYER = 'relayer' CONF_USER_POOL_ID = 'user_pool_id' +CONF_GOOGLE_ACTIONS_SYNC_URL = 'google_actions_sync_url' DEFAULT_MODE = 'production' DEPENDENCIES = ['http'] @@ -75,6 +76,7 @@ CONFIG_SCHEMA = vol.Schema({ vol.Optional(CONF_USER_POOL_ID): str, vol.Optional(CONF_REGION): str, vol.Optional(CONF_RELAYER): str, + vol.Optional(CONF_GOOGLE_ACTIONS_SYNC_URL): str, vol.Optional(CONF_ALEXA): ALEXA_SCHEMA, vol.Optional(CONF_GOOGLE_ACTIONS): GACTIONS_SCHEMA, }), @@ -110,7 +112,7 @@ class Cloud: def __init__(self, hass, mode, alexa, google_actions, cognito_client_id=None, user_pool_id=None, region=None, - relayer=None): + relayer=None, google_actions_sync_url=None): """Create an instance of Cloud.""" self.hass = hass self.mode = mode @@ -128,6 +130,7 @@ class Cloud: self.user_pool_id = user_pool_id self.region = region self.relayer = relayer + self.google_actions_sync_url = google_actions_sync_url else: info = SERVERS[mode] @@ -136,6 +139,7 @@ class Cloud: self.user_pool_id = info['user_pool_id'] self.region = info['region'] self.relayer = info['relayer'] + self.google_actions_sync_url = info['google_actions_sync_url'] @property def is_logged_in(self): diff --git a/homeassistant/components/cloud/const.py b/homeassistant/components/cloud/const.py index 99075d3d02d..82128206d47 100644 --- a/homeassistant/components/cloud/const.py +++ b/homeassistant/components/cloud/const.py @@ -8,7 +8,9 @@ SERVERS = { 'cognito_client_id': '60i2uvhvbiref2mftj7rgcrt9u', 'user_pool_id': 'us-east-1_87ll5WOP8', 'region': 'us-east-1', - 'relayer': 'wss://cloud.hass.io:8000/websocket' + 'relayer': 'wss://cloud.hass.io:8000/websocket', + 'google_actions_sync_url': ('https://24ab3v80xd.execute-api.us-east-1.' + 'amazonaws.com/prod/smart_home_sync'), } } diff --git a/homeassistant/components/cloud/http_api.py b/homeassistant/components/cloud/http_api.py index 3065de24180..a4b3b59f333 100644 --- a/homeassistant/components/cloud/http_api.py +++ b/homeassistant/components/cloud/http_api.py @@ -16,9 +16,9 @@ from .const import DOMAIN, REQUEST_TIMEOUT _LOGGER = logging.getLogger(__name__) -@asyncio.coroutine -def async_setup(hass): +async def async_setup(hass): """Initialize the HTTP API.""" + hass.http.register_view(GoogleActionsSyncView) hass.http.register_view(CloudLoginView) hass.http.register_view(CloudLogoutView) hass.http.register_view(CloudAccountView) @@ -38,12 +38,11 @@ _CLOUD_ERRORS = { def _handle_cloud_errors(handler): """Handle auth errors.""" - @asyncio.coroutine @wraps(handler) - def error_handler(view, request, *args, **kwargs): + async def error_handler(view, request, *args, **kwargs): """Handle exceptions that raise from the wrapped request handler.""" try: - result = yield from handler(view, request, *args, **kwargs) + result = await handler(view, request, *args, **kwargs) return result except (auth_api.CloudError, asyncio.TimeoutError) as err: @@ -57,6 +56,31 @@ def _handle_cloud_errors(handler): return error_handler +class GoogleActionsSyncView(HomeAssistantView): + """Trigger a Google Actions Smart Home Sync.""" + + url = '/api/cloud/google_actions/sync' + name = 'api:cloud:google_actions/sync' + + @_handle_cloud_errors + async def post(self, request): + """Trigger a Google Actions sync.""" + hass = request.app['hass'] + cloud = hass.data[DOMAIN] + websession = hass.helpers.aiohttp_client.async_get_clientsession() + + with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop): + await hass.async_add_job(auth_api.check_token, cloud) + + with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop): + req = await websession.post( + cloud.google_actions_sync_url, headers={ + 'authorization': cloud.id_token + }) + + return self.json({}, status_code=req.status) + + class CloudLoginView(HomeAssistantView): """Login to Home Assistant cloud.""" @@ -68,19 +92,18 @@ class CloudLoginView(HomeAssistantView): vol.Required('email'): str, vol.Required('password'): str, })) - @asyncio.coroutine - def post(self, request, data): + async def post(self, request, data): """Handle login request.""" hass = request.app['hass'] cloud = hass.data[DOMAIN] with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop): - yield from hass.async_add_job(auth_api.login, cloud, data['email'], - data['password']) + await hass.async_add_job(auth_api.login, cloud, data['email'], + data['password']) hass.async_add_job(cloud.iot.connect) # Allow cloud to start connecting. - yield from asyncio.sleep(0, loop=hass.loop) + await asyncio.sleep(0, loop=hass.loop) return self.json(_account_data(cloud)) @@ -91,14 +114,13 @@ class CloudLogoutView(HomeAssistantView): name = 'api:cloud:logout' @_handle_cloud_errors - @asyncio.coroutine - def post(self, request): + async def post(self, request): """Handle logout request.""" hass = request.app['hass'] cloud = hass.data[DOMAIN] with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop): - yield from cloud.logout() + await cloud.logout() return self.json_message('ok') @@ -109,8 +131,7 @@ class CloudAccountView(HomeAssistantView): url = '/api/cloud/account' name = 'api:cloud:account' - @asyncio.coroutine - def get(self, request): + async def get(self, request): """Get account info.""" hass = request.app['hass'] cloud = hass.data[DOMAIN] @@ -132,14 +153,13 @@ class CloudRegisterView(HomeAssistantView): vol.Required('email'): str, vol.Required('password'): vol.All(str, vol.Length(min=6)), })) - @asyncio.coroutine - def post(self, request, data): + async def post(self, request, data): """Handle registration request.""" hass = request.app['hass'] cloud = hass.data[DOMAIN] with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop): - yield from hass.async_add_job( + await hass.async_add_job( auth_api.register, cloud, data['email'], data['password']) return self.json_message('ok') @@ -155,14 +175,13 @@ class CloudResendConfirmView(HomeAssistantView): @RequestDataValidator(vol.Schema({ vol.Required('email'): str, })) - @asyncio.coroutine - def post(self, request, data): + async def post(self, request, data): """Handle resending confirm email code request.""" hass = request.app['hass'] cloud = hass.data[DOMAIN] with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop): - yield from hass.async_add_job( + await hass.async_add_job( auth_api.resend_email_confirm, cloud, data['email']) return self.json_message('ok') @@ -178,14 +197,13 @@ class CloudForgotPasswordView(HomeAssistantView): @RequestDataValidator(vol.Schema({ vol.Required('email'): str, })) - @asyncio.coroutine - def post(self, request, data): + async def post(self, request, data): """Handle forgot password request.""" hass = request.app['hass'] cloud = hass.data[DOMAIN] with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop): - yield from hass.async_add_job( + await hass.async_add_job( auth_api.forgot_password, cloud, data['email']) return self.json_message('ok') diff --git a/tests/components/cloud/test_http_api.py b/tests/components/cloud/test_http_api.py index 1ed3d1b4744..55c6290c158 100644 --- a/tests/components/cloud/test_http_api.py +++ b/tests/components/cloud/test_http_api.py @@ -11,6 +11,9 @@ from homeassistant.components.cloud import DOMAIN, auth_api, iot from tests.common import mock_coro +GOOGLE_ACTIONS_SYNC_URL = 'https://api-test.hass.io/google_actions_sync' + + @pytest.fixture def cloud_client(hass, aiohttp_client): """Fixture that can fetch from the cloud client.""" @@ -23,6 +26,7 @@ def cloud_client(hass, aiohttp_client): 'user_pool_id': 'user_pool_id', 'region': 'region', 'relayer': 'relayer', + 'google_actions_sync_url': GOOGLE_ACTIONS_SYNC_URL, } })) hass.data['cloud']._decode_claims = \ @@ -38,6 +42,21 @@ def mock_cognito(): yield mock_cog() +async def test_google_actions_sync(mock_cognito, cloud_client, aioclient_mock): + """Test syncing Google Actions.""" + aioclient_mock.post(GOOGLE_ACTIONS_SYNC_URL) + req = await cloud_client.post('/api/cloud/google_actions/sync') + assert req.status == 200 + + +async def test_google_actions_sync_fails(mock_cognito, cloud_client, + aioclient_mock): + """Test syncing Google Actions gone bad.""" + aioclient_mock.post(GOOGLE_ACTIONS_SYNC_URL, status=403) + req = await cloud_client.post('/api/cloud/google_actions/sync') + assert req.status == 403 + + @asyncio.coroutine def test_account_view_no_account(cloud_client): """Test fetching account if no account available.""" diff --git a/tests/components/cloud/test_init.py b/tests/components/cloud/test_init.py index 70990519a0b..91f8ab8316d 100644 --- a/tests/components/cloud/test_init.py +++ b/tests/components/cloud/test_init.py @@ -29,6 +29,7 @@ def test_constructor_loads_info_from_constant(): 'user_pool_id': 'test-user_pool_id', 'region': 'test-region', 'relayer': 'test-relayer', + 'google_actions_sync_url': 'test-google_actions_sync_url', } }), patch('homeassistant.components.cloud.Cloud._fetch_jwt_keyset', return_value=mock_coro(True)): @@ -43,6 +44,7 @@ def test_constructor_loads_info_from_constant(): assert cl.user_pool_id == 'test-user_pool_id' assert cl.region == 'test-region' assert cl.relayer == 'test-relayer' + assert cl.google_actions_sync_url == 'test-google_actions_sync_url' @asyncio.coroutine From 7fd687f59c7aa027f6d905fc9bd92504d4689430 Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Fri, 23 Mar 2018 21:54:19 +0100 Subject: [PATCH 182/220] Fix current_cover_position (#13135) --- homeassistant/components/cover/template.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/cover/template.py b/homeassistant/components/cover/template.py index f4728a12a3b..4e197365a70 100644 --- a/homeassistant/components/cover/template.py +++ b/homeassistant/components/cover/template.py @@ -234,7 +234,9 @@ class CoverTemplate(CoverDevice): None is unknown, 0 is closed, 100 is fully open. """ - return self._position + if self._position_template or self._position_script: + return self._position + return None @property def current_cover_tilt_position(self): From 630734ca152a04ff1fc6c9830626618e8a2397a8 Mon Sep 17 00:00:00 2001 From: Matt Hamrick Date: Fri, 23 Mar 2018 13:54:36 -0700 Subject: [PATCH 183/220] Switched values to downcase. (#13406) --- homeassistant/components/media_player/services.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/media_player/services.yaml b/homeassistant/components/media_player/services.yaml index beaea8a8ad0..95072f0270c 100644 --- a/homeassistant/components/media_player/services.yaml +++ b/homeassistant/components/media_player/services.yaml @@ -131,8 +131,8 @@ play_media: description: The ID of the content to play. Platform dependent. example: 'https://home-assistant.io/images/cast/splash.png' media_content_type: - description: The type of the content to play. Must be one of MUSIC, TVSHOW, VIDEO, EPISODE, CHANNEL or PLAYLIST - example: 'MUSIC' + description: The type of the content to play. Must be one of music, tvshow, video, episode, channel or playlist + example: 'music' select_source: description: Send the media player the command to change input source. From 6a625bdb37f4ccf85e89597518ca51ddc852e59c Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Fri, 23 Mar 2018 22:02:52 +0100 Subject: [PATCH 184/220] Cast Integration Cleanup (#13275) * Cast Integration Cleanup * Fix long line * Fixes and logging * Fix tests * Lint * Report unknown state with None * Lint * Switch to async_add_job Gets rid of those pesky "Setup of platform cast is taking over 10 seconds." messages. * Re-introduce PlatformNotReady * Add tests * Remove unnecessary checks * Test PlatformNotReady * Fix async in sync context * Blocking update It's not using async anyway * Upgrade pychromecast to 2.1.0 * Make reviewing easier I like "protected" access, but I like reviewing more :) * Make reviewing even easier :) * Comment tests --- homeassistant/components/media_player/cast.py | 576 +++++++++++------- requirements_all.txt | 2 +- tests/components/media_player/test_cast.py | 354 +++++++---- 3 files changed, 601 insertions(+), 331 deletions(-) diff --git a/homeassistant/components/media_player/cast.py b/homeassistant/components/media_player/cast.py index 579f9b62864..91b8d362c43 100644 --- a/homeassistant/components/media_player/cast.py +++ b/homeassistant/components/media_player/cast.py @@ -7,8 +7,10 @@ https://home-assistant.io/components/media_player.cast/ # pylint: disable=import-error import logging import threading +from typing import Optional, Tuple import voluptuous as vol +import attr from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.typing import HomeAssistantType, ConfigType @@ -22,11 +24,11 @@ from homeassistant.components.media_player import ( SUPPORT_STOP, SUPPORT_PLAY, MediaPlayerDevice, PLATFORM_SCHEMA) from homeassistant.const import ( CONF_HOST, STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING, - STATE_UNKNOWN, EVENT_HOMEASSISTANT_STOP) + EVENT_HOMEASSISTANT_STOP) import homeassistant.helpers.config_validation as cv import homeassistant.util.dt as dt_util -REQUIREMENTS = ['pychromecast==2.0.0'] +REQUIREMENTS = ['pychromecast==2.1.0'] _LOGGER = logging.getLogger(__name__) @@ -39,23 +41,103 @@ SUPPORT_CAST = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_PREVIOUS_TRACK | \ SUPPORT_NEXT_TRACK | SUPPORT_PLAY_MEDIA | SUPPORT_STOP | SUPPORT_PLAY +# Stores a threading.Lock that is held by the internal pychromecast discovery. INTERNAL_DISCOVERY_RUNNING_KEY = 'cast_discovery_running' -# UUID -> CastDevice mapping; cast devices without UUID are not stored +# Stores all ChromecastInfo we encountered through discovery or config as a set +# If we find a chromecast with a new host, the old one will be removed again. +KNOWN_CHROMECAST_INFO_KEY = 'cast_known_chromecasts' +# Stores UUIDs of cast devices that were added as entities. Doesn't store +# None UUIDs. ADDED_CAST_DEVICES_KEY = 'cast_added_cast_devices' -# Stores every discovered (host, port, uuid) -KNOWN_CHROMECASTS_KEY = 'cast_all_chromecasts' +# Dispatcher signal fired with a ChromecastInfo every time we discover a new +# Chromecast or receive it through configuration SIGNAL_CAST_DISCOVERED = 'cast_discovered' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_HOST): cv.string, - vol.Optional(CONF_IGNORE_CEC): [cv.string], + vol.Optional(CONF_IGNORE_CEC, default=[]): vol.All(cv.ensure_list, + [cv.string]) }) +@attr.s(slots=True, frozen=True) +class ChromecastInfo(object): + """Class to hold all data about a chromecast for creating connections. + + This also has the same attributes as the mDNS fields by zeroconf. + """ + + host = attr.ib(type=str) + port = attr.ib(type=int) + uuid = attr.ib(type=Optional[str], converter=attr.converters.optional(str), + default=None) # always convert UUID to string if not None + model_name = attr.ib(type=str, default='') # needed for cast type + friendly_name = attr.ib(type=Optional[str], default=None) + + @property + def is_audio_group(self) -> bool: + """Return if this is an audio group.""" + return self.port != DEFAULT_PORT + + @property + def is_information_complete(self) -> bool: + """Return if all information is filled out.""" + return all(attr.astuple(self)) + + @property + def host_port(self) -> Tuple[str, int]: + """Return the host+port tuple.""" + return self.host, self.port + + +def _fill_out_missing_chromecast_info(info: ChromecastInfo) -> ChromecastInfo: + """Fill out missing attributes of ChromecastInfo using blocking HTTP.""" + if info.is_information_complete or info.is_audio_group: + # We have all information, no need to check HTTP API. Or this is an + # audio group, so checking via HTTP won't give us any new information. + return info + + # Fill out missing information via HTTP dial. + from pychromecast import dial + + http_device_status = dial.get_device_status(info.host) + if http_device_status is None: + # HTTP dial didn't give us any new information. + return info + + return ChromecastInfo( + host=info.host, port=info.port, + uuid=(info.uuid or http_device_status.uuid), + friendly_name=(info.friendly_name or http_device_status.friendly_name), + model_name=(info.model_name or http_device_status.model_name) + ) + + +def _discover_chromecast(hass: HomeAssistantType, info: ChromecastInfo): + if info in hass.data[KNOWN_CHROMECAST_INFO_KEY]: + _LOGGER.debug("Discovered previous chromecast %s", info) + return + + # Either discovered completely new chromecast or a "moved" one. + info = _fill_out_missing_chromecast_info(info) + _LOGGER.debug("Discovered chromecast %s", info) + + if info.uuid is not None: + # Remove previous cast infos with same uuid from known chromecasts. + same_uuid = set(x for x in hass.data[KNOWN_CHROMECAST_INFO_KEY] + if info.uuid == x.uuid) + hass.data[KNOWN_CHROMECAST_INFO_KEY] -= same_uuid + + hass.data[KNOWN_CHROMECAST_INFO_KEY].add(info) + dispatcher_send(hass, SIGNAL_CAST_DISCOVERED, info) + + def _setup_internal_discovery(hass: HomeAssistantType) -> None: """Set up the pychromecast internal discovery.""" - hass.data.setdefault(INTERNAL_DISCOVERY_RUNNING_KEY, threading.Lock()) + if INTERNAL_DISCOVERY_RUNNING_KEY not in hass.data: + hass.data[INTERNAL_DISCOVERY_RUNNING_KEY] = threading.Lock() + if not hass.data[INTERNAL_DISCOVERY_RUNNING_KEY].acquire(blocking=False): # Internal discovery is already running return @@ -65,30 +147,14 @@ def _setup_internal_discovery(hass: HomeAssistantType) -> None: def internal_callback(name): """Called when zeroconf has discovered a new chromecast.""" mdns = listener.services[name] - ip_address, port, uuid, _, _ = mdns - key = (ip_address, port, uuid) - - if key in hass.data[KNOWN_CHROMECASTS_KEY]: - _LOGGER.debug("Discovered previous chromecast %s", mdns) - return - - _LOGGER.debug("Discovered new chromecast %s", mdns) - try: - # pylint: disable=protected-access - chromecast = pychromecast._get_chromecast_from_host( - mdns, blocking=True) - except pychromecast.ChromecastConnectionError: - _LOGGER.debug("Can't set up cast with mDNS info %s. " - "Assuming it's not a Chromecast", mdns) - return - hass.data[KNOWN_CHROMECASTS_KEY][key] = chromecast - dispatcher_send(hass, SIGNAL_CAST_DISCOVERED, chromecast) + _discover_chromecast(hass, ChromecastInfo(*mdns)) _LOGGER.debug("Starting internal pychromecast discovery.") listener, browser = pychromecast.start_discovery(internal_callback) def stop_discovery(event): """Stop discovery of new chromecasts.""" + _LOGGER.debug("Stopping internal pychromecast discovery.") pychromecast.stop_discovery(browser) hass.data[INTERNAL_DISCOVERY_RUNNING_KEY].release() @@ -96,40 +162,26 @@ def _setup_internal_discovery(hass: HomeAssistantType) -> None: @callback -def _async_create_cast_device(hass, chromecast): +def _async_create_cast_device(hass: HomeAssistantType, + info: ChromecastInfo): """Create a CastDevice Entity from the chromecast object. - Returns None if the cast device has already been added. Additionally, - automatically updates existing chromecast entities. + Returns None if the cast device has already been added. """ - if chromecast.uuid is None: + if info.uuid is None: # Found a cast without UUID, we don't store it because we won't be able # to update it anyway. - return CastDevice(chromecast) + return CastDevice(info) # Found a cast with UUID added_casts = hass.data[ADDED_CAST_DEVICES_KEY] - old_cast_device = added_casts.get(chromecast.uuid) - if old_cast_device is None: - # -> New cast device - cast_device = CastDevice(chromecast) - added_casts[chromecast.uuid] = cast_device - return cast_device - - old_key = (old_cast_device.cast.host, - old_cast_device.cast.port, - old_cast_device.cast.uuid) - new_key = (chromecast.host, chromecast.port, chromecast.uuid) - - if old_key == new_key: - # Re-discovered with same data, ignore + if info.uuid in added_casts: + # Already added this one, the entity will take care of moved hosts + # itself return None - - # -> Cast device changed host - # Remove old pychromecast.Chromecast from global list, because it isn't - # valid anymore - old_cast_device.async_set_chromecast(chromecast) - return None + # -> New cast device + added_casts.add(info.uuid) + return CastDevice(info) async def async_setup_platform(hass: HomeAssistantType, config: ConfigType, @@ -139,98 +191,308 @@ async def async_setup_platform(hass: HomeAssistantType, config: ConfigType, # Import CEC IGNORE attributes pychromecast.IGNORE_CEC += config.get(CONF_IGNORE_CEC, []) - hass.data.setdefault(ADDED_CAST_DEVICES_KEY, {}) - hass.data.setdefault(KNOWN_CHROMECASTS_KEY, {}) + hass.data.setdefault(ADDED_CAST_DEVICES_KEY, set()) + hass.data.setdefault(KNOWN_CHROMECAST_INFO_KEY, set()) - # None -> use discovery; (host, port) -> manually specify chromecast. - want_host = None - if discovery_info: - want_host = (discovery_info.get('host'), discovery_info.get('port')) + info = None + if discovery_info is not None: + info = ChromecastInfo(host=discovery_info['host'], + port=discovery_info['port']) elif CONF_HOST in config: - want_host = (config.get(CONF_HOST), DEFAULT_PORT) + info = ChromecastInfo(host=config[CONF_HOST], + port=DEFAULT_PORT) - enable_discovery = False - if want_host is None: - # We were explicitly told to enable pychromecast discovery. - enable_discovery = True - elif want_host[1] != DEFAULT_PORT: - # We're trying to add a group, so we have to use pychromecast's - # discovery to get the correct friendly name. - enable_discovery = True + @callback + def async_cast_discovered(discover: ChromecastInfo) -> None: + """Callback for when a new chromecast is discovered.""" + if info is not None and info.host_port != discover.host_port: + # Not our requested cast device. + return - if enable_discovery: - @callback - def async_cast_discovered(chromecast): - """Callback for when a new chromecast is discovered.""" - if want_host is not None and \ - (chromecast.host, chromecast.port) != want_host: - return # for groups, only add requested device - cast_device = _async_create_cast_device(hass, chromecast) + cast_device = _async_create_cast_device(hass, discover) + if cast_device is not None: + async_add_devices([cast_device]) - if cast_device is not None: - async_add_devices([cast_device]) - - async_dispatcher_connect(hass, SIGNAL_CAST_DISCOVERED, - async_cast_discovered) - # Re-play the callback for all past chromecasts, store the objects in - # a list to avoid concurrent modification resulting in exception. - for chromecast in list(hass.data[KNOWN_CHROMECASTS_KEY].values()): - async_cast_discovered(chromecast) + async_dispatcher_connect(hass, SIGNAL_CAST_DISCOVERED, + async_cast_discovered) + # Re-play the callback for all past chromecasts, store the objects in + # a list to avoid concurrent modification resulting in exception. + for chromecast in list(hass.data[KNOWN_CHROMECAST_INFO_KEY]): + async_cast_discovered(chromecast) + if info is None or info.is_audio_group: + # If we were a) explicitly told to enable discovery or + # b) have an audio group cast device, we need internal discovery. hass.async_add_job(_setup_internal_discovery, hass) else: - # Manually add a "normal" Chromecast, we can do that without discovery. - try: - chromecast = await hass.async_add_job( - pychromecast.Chromecast, *want_host) - except pychromecast.ChromecastConnectionError as err: - _LOGGER.warning("Can't set up chromecast on %s: %s", - want_host[0], err) + info = await hass.async_add_job(_fill_out_missing_chromecast_info, + info) + if info.friendly_name is None: + # HTTP dial failed, so we won't be able to connect. raise PlatformNotReady - key = (chromecast.host, chromecast.port, chromecast.uuid) - cast_device = _async_create_cast_device(hass, chromecast) - if cast_device is not None: - hass.data[KNOWN_CHROMECASTS_KEY][key] = chromecast - async_add_devices([cast_device]) + hass.async_add_job(_discover_chromecast, hass, info) + + +class CastStatusListener(object): + """Helper class to handle pychromecast status callbacks. + + Necessary because a CastDevice entity can create a new socket client + and therefore callbacks from multiple chromecast connections can + potentially arrive. This class allows invalidating past chromecast objects. + """ + + def __init__(self, cast_device, chromecast): + """Initialize the status listener.""" + self._cast_device = cast_device + self._valid = True + + chromecast.register_status_listener(self) + chromecast.socket_client.media_controller.register_status_listener( + self) + chromecast.register_connection_listener(self) + + def new_cast_status(self, cast_status): + """Called when a new CastStatus is received.""" + if self._valid: + self._cast_device.new_cast_status(cast_status) + + def new_media_status(self, media_status): + """Called when a new MediaStatus is received.""" + if self._valid: + self._cast_device.new_media_status(media_status) + + def new_connection_status(self, connection_status): + """Called when a new ConnectionStatus is received.""" + if self._valid: + self._cast_device.new_connection_status(connection_status) + + def invalidate(self): + """Invalidate this status listener. + + All following callbacks won't be forwarded. + """ + self._valid = False class CastDevice(MediaPlayerDevice): - """Representation of a Cast device on the network.""" + """Representation of a Cast device on the network. - def __init__(self, chromecast): - """Initialize the Cast device.""" - self.cast = None # type: pychromecast.Chromecast + This class is the holder of the pychromecast.Chromecast object and its + socket client. It therefore handles all reconnects and audio group changing + "elected leader" itself. + """ + + def __init__(self, cast_info): + """Initialize the cast device.""" + self._cast_info = cast_info # type: ChromecastInfo + self._chromecast = None # type: Optional[pychromecast.Chromecast] self.cast_status = None self.media_status = None self.media_status_received = None + self._available = False # type: bool + self._status_listener = None # type: Optional[CastStatusListener] - self.async_set_chromecast(chromecast) + async def async_added_to_hass(self): + """Create chromecast object when added to hass.""" + @callback + def async_cast_discovered(discover: ChromecastInfo): + """Callback for changing elected leaders / IP.""" + if self._cast_info.uuid is None: + # We can't handle empty UUIDs + return + if self._cast_info.uuid != discover.uuid: + # Discovered is not our device. + return + _LOGGER.debug("Discovered chromecast with same UUID: %s", discover) + self.hass.async_add_job(self.async_set_cast_info(discover)) + async_dispatcher_connect(self.hass, SIGNAL_CAST_DISCOVERED, + async_cast_discovered) + self.hass.async_add_job(self.async_set_cast_info(self._cast_info)) + + async def async_will_remove_from_hass(self) -> None: + """Disconnect Chromecast object when removed.""" + self._async_disconnect() + if self._cast_info.uuid is not None: + # Remove the entity from the added casts so that it can dynamically + # be re-added again. + self.hass.data[ADDED_CAST_DEVICES_KEY].remove(self._cast_info.uuid) + + async def async_set_cast_info(self, cast_info): + """Set the cast information and set up the chromecast object.""" + import pychromecast + old_cast_info = self._cast_info + self._cast_info = cast_info + + if self._chromecast is not None: + if old_cast_info.host_port == cast_info.host_port: + # Nothing connection-related updated + return + self._async_disconnect() + + # Failed connection will unfortunately never raise an exception, it + # will instead just try connecting indefinitely. + # pylint: disable=protected-access + _LOGGER.debug("Connecting to cast device %s", cast_info) + chromecast = await self.hass.async_add_job( + pychromecast._get_chromecast_from_host, attr.astuple(cast_info)) + self._chromecast = chromecast + self._status_listener = CastStatusListener(self, chromecast) + # Initialise connection status as connected because we can only + # register the connection listener *after* the initial connection + # attempt. If the initial connection failed, we would never reach + # this code anyway. + self._available = True + self.cast_status = chromecast.status + self.media_status = chromecast.media_controller.status + _LOGGER.debug("Connection successful!") + self.async_schedule_update_ha_state() + + @callback + def _async_disconnect(self): + """Disconnect Chromecast object if it is set.""" + if self._chromecast is None: + # Can't disconnect if not connected. + return + _LOGGER.debug("Disconnecting from previous chromecast socket.") + self._available = False + self._chromecast.disconnect(blocking=False) + # Invalidate some attributes + self._chromecast = None + self.cast_status = None + self.media_status = None + self.media_status_received = None + self._status_listener.invalidate() + self._status_listener = None + + def update(self): + """Periodically update the properties. + + Even though we receive callbacks for most state changes, some 3rd party + apps don't always send them. Better poll every now and then if the + chromecast is active (i.e. an app is running). + """ + if not self._available: + # Not connected or not available. + return + + if self._chromecast.media_controller.is_active: + # We can only update status if the media namespace is active + self._chromecast.media_controller.update_status() + + # ========== Callbacks ========== + def new_cast_status(self, cast_status): + """Handle updates of the cast status.""" + self.cast_status = cast_status + self.schedule_update_ha_state() + + def new_media_status(self, media_status): + """Handle updates of the media status.""" + self.media_status = media_status + self.media_status_received = dt_util.utcnow() + self.schedule_update_ha_state() + + def new_connection_status(self, connection_status): + """Handle updates of connection status.""" + from pychromecast.socket_client import CONNECTION_STATUS_CONNECTED + + new_available = connection_status.status == CONNECTION_STATUS_CONNECTED + if new_available != self._available: + # Connection status callbacks happen often when disconnected. + # Only update state when availability changed to put less pressure + # on state machine. + _LOGGER.debug("Cast device availability changed: %s", + connection_status.status) + self._available = new_available + self.schedule_update_ha_state() + + # ========== Service Calls ========== + def turn_on(self): + """Turn on the cast device.""" + import pychromecast + + if not self._chromecast.is_idle: + # Already turned on + return + + if self._chromecast.app_id is not None: + # Quit the previous app before starting splash screen + self._chromecast.quit_app() + + # The only way we can turn the Chromecast is on is by launching an app + self._chromecast.play_media(CAST_SPLASH, + pychromecast.STREAM_TYPE_BUFFERED) + + def turn_off(self): + """Turn off the cast device.""" + self._chromecast.quit_app() + + def mute_volume(self, mute): + """Mute the volume.""" + self._chromecast.set_volume_muted(mute) + + def set_volume_level(self, volume): + """Set volume level, range 0..1.""" + self._chromecast.set_volume(volume) + + def media_play(self): + """Send play command.""" + self._chromecast.media_controller.play() + + def media_pause(self): + """Send pause command.""" + self._chromecast.media_controller.pause() + + def media_stop(self): + """Send stop command.""" + self._chromecast.media_controller.stop() + + def media_previous_track(self): + """Send previous track command.""" + self._chromecast.media_controller.rewind() + + def media_next_track(self): + """Send next track command.""" + self._chromecast.media_controller.skip() + + def media_seek(self, position): + """Seek the media to a specific location.""" + self._chromecast.media_controller.seek(position) + + def play_media(self, media_type, media_id, **kwargs): + """Play media from a URL.""" + self._chromecast.media_controller.play_media(media_id, media_type) + + # ========== Properties ========== @property def should_poll(self): - """No polling needed.""" - return False + """Polling needed for cast integration, see async_update.""" + return True @property def name(self): """Return the name of the device.""" - return self.cast.device.friendly_name + return self._cast_info.friendly_name - # MediaPlayerDevice properties and methods @property def state(self): """Return the state of the player.""" if self.media_status is None: - return STATE_UNKNOWN + return None elif self.media_status.player_is_playing: return STATE_PLAYING elif self.media_status.player_is_paused: return STATE_PAUSED elif self.media_status.player_is_idle: return STATE_IDLE - elif self.cast.is_idle: + elif self._chromecast is not None and self._chromecast.is_idle: return STATE_OFF - return STATE_UNKNOWN + return None + + @property + def available(self): + """Return True if the cast device is connected.""" + return self._available @property def volume_level(self): @@ -318,12 +580,12 @@ class CastDevice(MediaPlayerDevice): @property def app_id(self): """Return the ID of the current running app.""" - return self.cast.app_id + return self._chromecast.app_id if self._chromecast else None @property def app_name(self): """Name of the current running app.""" - return self.cast.app_display_name + return self._chromecast.app_display_name if self._chromecast else None @property def supported_features(self): @@ -349,101 +611,7 @@ class CastDevice(MediaPlayerDevice): """ return self.media_status_received - def turn_on(self): - """Turn on the ChromeCast.""" - # The only way we can turn the Chromecast is on is by launching an app - if not self.cast.status or not self.cast.status.is_active_input: - import pychromecast - - if self.cast.app_id: - self.cast.quit_app() - - self.cast.play_media( - CAST_SPLASH, pychromecast.STREAM_TYPE_BUFFERED) - - def turn_off(self): - """Turn Chromecast off.""" - self.cast.quit_app() - - def mute_volume(self, mute): - """Mute the volume.""" - self.cast.set_volume_muted(mute) - - def set_volume_level(self, volume): - """Set volume level, range 0..1.""" - self.cast.set_volume(volume) - - def media_play(self): - """Send play command.""" - self.cast.media_controller.play() - - def media_pause(self): - """Send pause command.""" - self.cast.media_controller.pause() - - def media_stop(self): - """Send stop command.""" - self.cast.media_controller.stop() - - def media_previous_track(self): - """Send previous track command.""" - self.cast.media_controller.rewind() - - def media_next_track(self): - """Send next track command.""" - self.cast.media_controller.skip() - - def media_seek(self, position): - """Seek the media to a specific location.""" - self.cast.media_controller.seek(position) - - def play_media(self, media_type, media_id, **kwargs): - """Play media from a URL.""" - self.cast.media_controller.play_media(media_id, media_type) - - # Implementation of chromecast status_listener methods - def new_cast_status(self, status): - """Handle updates of the cast status.""" - self.cast_status = status - self.schedule_update_ha_state() - - def new_media_status(self, status): - """Handle updates of the media status.""" - self.media_status = status - self.media_status_received = dt_util.utcnow() - self.schedule_update_ha_state() - @property - def unique_id(self) -> str: + def unique_id(self) -> Optional[str]: """Return a unique ID.""" - if self.cast.uuid is not None: - return str(self.cast.uuid) - return None - - @callback - def async_set_chromecast(self, chromecast): - """Set the internal Chromecast object and disconnect the previous.""" - self._async_disconnect() - - self.cast = chromecast - - self.cast.socket_client.receiver_controller.register_status_listener( - self) - self.cast.socket_client.media_controller.register_status_listener(self) - - self.cast_status = self.cast.status - self.media_status = self.cast.media_controller.status - - async def async_will_remove_from_hass(self) -> None: - """Disconnect Chromecast object when removed.""" - self._async_disconnect() - - @callback - def _async_disconnect(self): - """Disconnect Chromecast object if it is set.""" - if self.cast is None: - return - _LOGGER.debug("Disconnecting existing chromecast object") - old_key = (self.cast.host, self.cast.port, self.cast.uuid) - self.hass.data[KNOWN_CHROMECASTS_KEY].pop(old_key) - self.cast.disconnect(blocking=False) + return self._cast_info.uuid diff --git a/requirements_all.txt b/requirements_all.txt index 75fd6de8f46..52833969872 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -685,7 +685,7 @@ pybbox==0.0.5-alpha pychannels==1.0.0 # homeassistant.components.media_player.cast -pychromecast==2.0.0 +pychromecast==2.1.0 # homeassistant.components.media_player.cmus pycmus==0.1.0 diff --git a/tests/components/media_player/test_cast.py b/tests/components/media_player/test_cast.py index 2075b4cf6e6..ee69ec1c85d 100644 --- a/tests/components/media_player/test_cast.py +++ b/tests/components/media_player/test_cast.py @@ -5,12 +5,17 @@ from typing import Optional from unittest.mock import patch, MagicMock, Mock from uuid import UUID +import attr import pytest from homeassistant.exceptions import PlatformNotReady +from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.components.media_player.cast import ChromecastInfo from homeassistant.const import EVENT_HOMEASSISTANT_STOP -from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.dispatcher import async_dispatcher_connect, \ + async_dispatcher_send from homeassistant.components.media_player import cast +from homeassistant.setup import async_setup_component @pytest.fixture(autouse=True) @@ -26,57 +31,74 @@ def cast_mock(): FakeUUID = UUID('57355bce-9364-4aa6-ac1e-eb849dccf9e2') -def get_fake_chromecast(host='192.168.178.42', port=8009, - uuid: Optional[UUID] = FakeUUID): +def get_fake_chromecast(info: ChromecastInfo): """Generate a Fake Chromecast object with the specified arguments.""" - return MagicMock(host=host, port=port, uuid=uuid) + mock = MagicMock(host=info.host, port=info.port, uuid=info.uuid) + mock.media_controller.status = None + return mock -@asyncio.coroutine -def async_setup_cast(hass, config=None, discovery_info=None): +def get_fake_chromecast_info(host='192.168.178.42', port=8009, + uuid: Optional[UUID] = FakeUUID): + """Generate a Fake ChromecastInfo with the specified arguments.""" + return ChromecastInfo(host=host, port=port, uuid=uuid, + friendly_name="Speaker") + + +async def async_setup_cast(hass, config=None, discovery_info=None): """Helper to setup the cast platform.""" if config is None: config = {} add_devices = Mock() - yield from cast.async_setup_platform(hass, config, add_devices, - discovery_info=discovery_info) - yield from hass.async_block_till_done() + await cast.async_setup_platform(hass, config, add_devices, + discovery_info=discovery_info) + await hass.async_block_till_done() return add_devices -@asyncio.coroutine -def async_setup_cast_internal_discovery(hass, config=None, - discovery_info=None, - no_from_host_patch=False): +async def async_setup_cast_internal_discovery(hass, config=None, + discovery_info=None): """Setup the cast platform and the discovery.""" listener = MagicMock(services={}) with patch('pychromecast.start_discovery', return_value=(listener, None)) as start_discovery: - add_devices = yield from async_setup_cast(hass, config, discovery_info) - yield from hass.async_block_till_done() - yield from hass.async_block_till_done() + add_devices = await async_setup_cast(hass, config, discovery_info) + await hass.async_block_till_done() + await hass.async_block_till_done() assert start_discovery.call_count == 1 discovery_callback = start_discovery.call_args[0][0] - def discover_chromecast(service_name, chromecast): + def discover_chromecast(service_name: str, info: ChromecastInfo) -> None: """Discover a chromecast device.""" - listener.services[service_name] = ( - chromecast.host, chromecast.port, chromecast.uuid, None, None) - if no_from_host_patch: - discovery_callback(service_name) - else: - with patch('pychromecast._get_chromecast_from_host', - return_value=chromecast): - discovery_callback(service_name) + listener.services[service_name] = attr.astuple(info) + discovery_callback(service_name) return discover_chromecast, add_devices +async def async_setup_media_player_cast(hass: HomeAssistantType, + info: ChromecastInfo): + """Setup the cast platform with async_setup_component.""" + chromecast = get_fake_chromecast(info) + + cast.CastStatusListener = MagicMock() + + with patch('pychromecast._get_chromecast_from_host', + return_value=chromecast) as get_chromecast: + await async_setup_component(hass, 'media_player', { + 'media_player': {'platform': 'cast', 'host': info.host}}) + await hass.async_block_till_done() + assert get_chromecast.call_count == 1 + assert cast.CastStatusListener.call_count == 1 + entity = cast.CastStatusListener.call_args[0][0] + return chromecast, entity + + @asyncio.coroutine def test_start_discovery_called_once(hass): """Test pychromecast.start_discovery called exactly once.""" @@ -95,11 +117,13 @@ def test_stop_discovery_called_on_stop(hass): """Test pychromecast.stop_discovery called on shutdown.""" with patch('pychromecast.start_discovery', return_value=(None, 'the-browser')) as start_discovery: - yield from async_setup_cast(hass) + # start_discovery should be called with empty config + yield from async_setup_cast(hass, {}) assert start_discovery.call_count == 1 with patch('pychromecast.stop_discovery') as stop_discovery: + # stop discovery should be called on shutdown hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) yield from hass.async_block_till_done() @@ -107,145 +131,223 @@ def test_stop_discovery_called_on_stop(hass): with patch('pychromecast.start_discovery', return_value=(None, 'the-browser')) as start_discovery: + # start_discovery should be called again on re-startup yield from async_setup_cast(hass) assert start_discovery.call_count == 1 -@asyncio.coroutine -def test_internal_discovery_callback_only_generates_once(hass): - """Test _get_chromecast_from_host only called once per device.""" - discover_cast, _ = yield from async_setup_cast_internal_discovery( - hass, no_from_host_patch=True) - chromecast = get_fake_chromecast() +async def test_internal_discovery_callback_only_generates_once(hass): + """Test discovery only called once per device.""" + discover_cast, _ = await async_setup_cast_internal_discovery(hass) + info = get_fake_chromecast_info() - with patch('pychromecast._get_chromecast_from_host', - return_value=chromecast) as gen_chromecast: - discover_cast('the-service', chromecast) - mdns = (chromecast.host, chromecast.port, chromecast.uuid, None, None) - gen_chromecast.assert_called_once_with(mdns, blocking=True) + signal = MagicMock() + async_dispatcher_connect(hass, 'cast_discovered', signal) - discover_cast('the-service', chromecast) - gen_chromecast.reset_mock() - assert gen_chromecast.call_count == 0 - - -@asyncio.coroutine -def test_internal_discovery_callback_calls_dispatcher(hass): - """Test internal discovery calls dispatcher.""" - discover_cast, _ = yield from async_setup_cast_internal_discovery(hass) - chromecast = get_fake_chromecast() - - with patch('pychromecast._get_chromecast_from_host', - return_value=chromecast): - signal = MagicMock() - - async_dispatcher_connect(hass, 'cast_discovered', signal) - discover_cast('the-service', chromecast) - yield from hass.async_block_till_done() - - signal.assert_called_once_with(chromecast) - - -@asyncio.coroutine -def test_internal_discovery_callback_with_connection_error(hass): - """Test internal discovery not calling dispatcher on ConnectionError.""" - import pychromecast # imports mock pychromecast - - pychromecast.ChromecastConnectionError = IOError - - discover_cast, _ = yield from async_setup_cast_internal_discovery( - hass, no_from_host_patch=True) - chromecast = get_fake_chromecast() - - with patch('pychromecast._get_chromecast_from_host', - side_effect=pychromecast.ChromecastConnectionError): - signal = MagicMock() - - async_dispatcher_connect(hass, 'cast_discovered', signal) - discover_cast('the-service', chromecast) - yield from hass.async_block_till_done() + with patch('pychromecast.dial.get_device_status', return_value=None): + # discovering a cast device should call the dispatcher + discover_cast('the-service', info) + await hass.async_block_till_done() + discover = signal.mock_calls[0][1][0] + # attr's __eq__ somehow breaks here, use tuples instead + assert attr.astuple(discover) == attr.astuple(info) + signal.reset_mock() + # discovering it a second time shouldn't + discover_cast('the-service', info) + await hass.async_block_till_done() assert signal.call_count == 0 -def test_create_cast_device_without_uuid(hass): - """Test create a cast device without a UUID.""" - chromecast = get_fake_chromecast(uuid=None) - cast_device = cast._async_create_cast_device(hass, chromecast) - assert cast_device is not None - - -def test_create_cast_device_with_uuid(hass): - """Test create cast devices with UUID.""" - added_casts = hass.data[cast.ADDED_CAST_DEVICES_KEY] = {} - chromecast = get_fake_chromecast() - cast_device = cast._async_create_cast_device(hass, chromecast) - assert cast_device is not None - assert chromecast.uuid in added_casts - - with patch.object(cast_device, 'async_set_chromecast') as mock_set: - assert cast._async_create_cast_device(hass, chromecast) is None - assert mock_set.call_count == 0 - - chromecast = get_fake_chromecast(host='192.168.178.1') - assert cast._async_create_cast_device(hass, chromecast) is None - assert mock_set.call_count == 1 - mock_set.assert_called_once_with(chromecast) - - -@asyncio.coroutine -def test_normal_chromecast_not_starting_discovery(hass): - """Test cast platform not starting discovery when not required.""" +async def test_internal_discovery_callback_fill_out(hass): + """Test internal discovery automatically filling out information.""" import pychromecast # imports mock pychromecast pychromecast.ChromecastConnectionError = IOError - chromecast = get_fake_chromecast() + discover_cast, _ = await async_setup_cast_internal_discovery(hass) + info = get_fake_chromecast_info(uuid=None) + full_info = attr.evolve(info, model_name='google home', + friendly_name='Speaker', uuid=FakeUUID) - with patch('pychromecast.Chromecast', return_value=chromecast): - add_devices = yield from async_setup_cast(hass, {'host': 'host1'}) + with patch('pychromecast.dial.get_device_status', + return_value=full_info): + signal = MagicMock() + + async_dispatcher_connect(hass, 'cast_discovered', signal) + discover_cast('the-service', info) + await hass.async_block_till_done() + + # when called with incomplete info, it should use HTTP to get missing + discover = signal.mock_calls[0][1][0] + # attr's __eq__ somehow breaks here, use tuples instead + assert attr.astuple(discover) == attr.astuple(full_info) + + +async def test_create_cast_device_without_uuid(hass): + """Test create a cast device with no UUId should still create an entity.""" + info = get_fake_chromecast_info(uuid=None) + cast_device = cast._async_create_cast_device(hass, info) + assert cast_device is not None + + +async def test_create_cast_device_with_uuid(hass): + """Test create cast devices with UUID creates entities.""" + added_casts = hass.data[cast.ADDED_CAST_DEVICES_KEY] = set() + info = get_fake_chromecast_info() + + cast_device = cast._async_create_cast_device(hass, info) + assert cast_device is not None + assert info.uuid in added_casts + + # Sending second time should not create new entity + cast_device = cast._async_create_cast_device(hass, info) + assert cast_device is None + + +async def test_normal_chromecast_not_starting_discovery(hass): + """Test cast platform not starting discovery when not required.""" + # pylint: disable=no-member + with patch('homeassistant.components.media_player.cast.' + '_setup_internal_discovery') as setup_discovery: + # normal (non-group) chromecast shouldn't start discovery. + add_devices = await async_setup_cast(hass, {'host': 'host1'}) + await hass.async_block_till_done() assert add_devices.call_count == 1 + assert setup_discovery.call_count == 0 # Same entity twice - add_devices = yield from async_setup_cast(hass, {'host': 'host1'}) + add_devices = await async_setup_cast(hass, {'host': 'host1'}) + await hass.async_block_till_done() assert add_devices.call_count == 0 + assert setup_discovery.call_count == 0 - hass.data[cast.ADDED_CAST_DEVICES_KEY] = {} - add_devices = yield from async_setup_cast( + hass.data[cast.ADDED_CAST_DEVICES_KEY] = set() + add_devices = await async_setup_cast( hass, discovery_info={'host': 'host1', 'port': 8009}) + await hass.async_block_till_done() assert add_devices.call_count == 1 + assert setup_discovery.call_count == 0 - hass.data[cast.ADDED_CAST_DEVICES_KEY] = {} - add_devices = yield from async_setup_cast( + # group should start discovery. + hass.data[cast.ADDED_CAST_DEVICES_KEY] = set() + add_devices = await async_setup_cast( hass, discovery_info={'host': 'host1', 'port': 42}) + await hass.async_block_till_done() assert add_devices.call_count == 0 + assert setup_discovery.call_count == 1 - with patch('pychromecast.Chromecast', - side_effect=pychromecast.ChromecastConnectionError): + +async def test_normal_raises_platform_not_ready(hass): + """Test cast platform raises PlatformNotReady if HTTP dial fails.""" + with patch('pychromecast.dial.get_device_status', return_value=None): with pytest.raises(PlatformNotReady): - yield from async_setup_cast(hass, {'host': 'host3'}) + await async_setup_cast(hass, {'host': 'host1'}) -@asyncio.coroutine -def test_replay_past_chromecasts(hass): +async def test_replay_past_chromecasts(hass): """Test cast platform re-playing past chromecasts when adding new one.""" - cast_group1 = get_fake_chromecast(host='host1', port=42) - cast_group2 = get_fake_chromecast(host='host2', port=42, uuid=UUID( + cast_group1 = get_fake_chromecast_info(host='host1', port=42) + cast_group2 = get_fake_chromecast_info(host='host2', port=42, uuid=UUID( '9462202c-e747-4af5-a66b-7dce0e1ebc09')) - discover_cast, add_dev1 = yield from async_setup_cast_internal_discovery( + discover_cast, add_dev1 = await async_setup_cast_internal_discovery( hass, discovery_info={'host': 'host1', 'port': 42}) discover_cast('service2', cast_group2) - yield from hass.async_block_till_done() + await hass.async_block_till_done() assert add_dev1.call_count == 0 discover_cast('service1', cast_group1) - yield from hass.async_block_till_done() - yield from hass.async_block_till_done() # having jobs that add jobs + await hass.async_block_till_done() + await hass.async_block_till_done() # having tasks that add jobs assert add_dev1.call_count == 1 - add_dev2 = yield from async_setup_cast( + add_dev2 = await async_setup_cast( hass, discovery_info={'host': 'host2', 'port': 42}) - yield from hass.async_block_till_done() + await hass.async_block_till_done() assert add_dev2.call_count == 1 + + +async def test_entity_media_states(hass: HomeAssistantType): + """Test various entity media states.""" + info = get_fake_chromecast_info() + full_info = attr.evolve(info, model_name='google home', + friendly_name='Speaker', uuid=FakeUUID) + + with patch('pychromecast.dial.get_device_status', + return_value=full_info): + chromecast, entity = await async_setup_media_player_cast(hass, info) + + state = hass.states.get('media_player.speaker') + assert state is not None + assert state.name == 'Speaker' + assert state.state == 'unknown' + assert entity.unique_id == full_info.uuid + + media_status = MagicMock(images=None) + media_status.player_is_playing = True + entity.new_media_status(media_status) + await hass.async_block_till_done() + state = hass.states.get('media_player.speaker') + assert state.state == 'playing' + + entity.new_media_status(media_status) + media_status.player_is_playing = False + media_status.player_is_paused = True + await hass.async_block_till_done() + state = hass.states.get('media_player.speaker') + assert state.state == 'paused' + + entity.new_media_status(media_status) + media_status.player_is_paused = False + media_status.player_is_idle = True + await hass.async_block_till_done() + state = hass.states.get('media_player.speaker') + assert state.state == 'idle' + + media_status.player_is_idle = False + chromecast.is_idle = True + entity.new_media_status(media_status) + await hass.async_block_till_done() + state = hass.states.get('media_player.speaker') + assert state.state == 'off' + + chromecast.is_idle = False + entity.new_media_status(media_status) + await hass.async_block_till_done() + state = hass.states.get('media_player.speaker') + assert state.state == 'unknown' + + +async def test_switched_host(hass: HomeAssistantType): + """Test cast device listens for changed hosts and disconnects old cast.""" + info = get_fake_chromecast_info() + full_info = attr.evolve(info, model_name='google home', + friendly_name='Speaker', uuid=FakeUUID) + + with patch('pychromecast.dial.get_device_status', + return_value=full_info): + chromecast, _ = await async_setup_media_player_cast(hass, full_info) + + chromecast2 = get_fake_chromecast(info) + with patch('pychromecast._get_chromecast_from_host', + return_value=chromecast2) as get_chromecast: + async_dispatcher_send(hass, cast.SIGNAL_CAST_DISCOVERED, full_info) + await hass.async_block_till_done() + assert get_chromecast.call_count == 0 + + changed = attr.evolve(full_info, friendly_name='Speaker 2') + async_dispatcher_send(hass, cast.SIGNAL_CAST_DISCOVERED, changed) + await hass.async_block_till_done() + assert get_chromecast.call_count == 0 + + changed = attr.evolve(changed, host='host2') + async_dispatcher_send(hass, cast.SIGNAL_CAST_DISCOVERED, changed) + await hass.async_block_till_done() + assert get_chromecast.call_count == 1 + chromecast.disconnect.assert_called_once_with(blocking=False) + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + chromecast.disconnect.assert_called_once_with(blocking=False) From 725e1ddfc116ff6f06e5d7574321ffc96d30faf8 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 23 Mar 2018 14:15:31 -0700 Subject: [PATCH 185/220] Update translations --- .../.translations/nl.json | 3 ++- .../.translations/vi.json | 24 +++++++++++++++++++ .../components/hue/.translations/de.json | 2 +- .../components/hue/.translations/pl.json | 2 +- .../sensor/.translations/season.vi.json | 8 +++++++ 5 files changed, 36 insertions(+), 3 deletions(-) create mode 100644 homeassistant/components/config_entry_example/.translations/vi.json create mode 100644 homeassistant/components/sensor/.translations/season.vi.json diff --git a/homeassistant/components/config_entry_example/.translations/nl.json b/homeassistant/components/config_entry_example/.translations/nl.json index 10469dd0804..7b52ac88cf0 100644 --- a/homeassistant/components/config_entry_example/.translations/nl.json +++ b/homeassistant/components/config_entry_example/.translations/nl.json @@ -18,6 +18,7 @@ "description": "Voer een naam in voor het testen van de entiteit.", "title": "Naam van de entiteit" } - } + }, + "title": "Voorbeeld van de config vermelding" } } \ No newline at end of file diff --git a/homeassistant/components/config_entry_example/.translations/vi.json b/homeassistant/components/config_entry_example/.translations/vi.json new file mode 100644 index 00000000000..e40c4d38e9f --- /dev/null +++ b/homeassistant/components/config_entry_example/.translations/vi.json @@ -0,0 +1,24 @@ +{ + "config": { + "error": { + "invalid_object_id": "ID \u0111\u1ed1i t\u01b0\u1ee3ng kh\u00f4ng h\u1ee3p l\u1ec7" + }, + "step": { + "init": { + "data": { + "object_id": "ID \u0111\u1ed1i t\u01b0\u1ee3ng" + }, + "description": "Xin vui l\u00f2ng nh\u1eadp m\u1ed9t object_id cho th\u1eed nghi\u1ec7m th\u1ef1c th\u1ec3.", + "title": "Ch\u1ecdn id \u0111\u1ed1i t\u01b0\u1ee3ng" + }, + "name": { + "data": { + "name": "T\u00ean" + }, + "description": "Xin vui l\u00f2ng nh\u1eadp t\u00ean cho th\u1eed nghi\u1ec7m th\u1ef1c th\u1ec3.", + "title": "T\u00ean c\u1ee7a th\u1ef1c th\u1ec3" + } + }, + "title": "V\u00ed d\u1ee5 v\u1ec1 c\u1ea5u h\u00ecnh th\u1ef1c th\u1ec3" + } +} \ No newline at end of file diff --git a/homeassistant/components/hue/.translations/de.json b/homeassistant/components/hue/.translations/de.json index b7094d91528..f11af7756c7 100644 --- a/homeassistant/components/hue/.translations/de.json +++ b/homeassistant/components/hue/.translations/de.json @@ -6,7 +6,7 @@ "no_bridges": "Philips Hue Bridges entdeckt" }, "error": { - "linking": "Unbekannte Link-Fehler aufgetreten.", + "linking": "Unbekannter Link-Fehler aufgetreten.", "register_failed": "Registrieren fehlgeschlagen, bitte versuche es erneut" }, "step": { diff --git a/homeassistant/components/hue/.translations/pl.json b/homeassistant/components/hue/.translations/pl.json index cdd26a5b4b2..e364b7033a1 100644 --- a/homeassistant/components/hue/.translations/pl.json +++ b/homeassistant/components/hue/.translations/pl.json @@ -17,7 +17,7 @@ "title": "Wybierz mostek Hue" }, "link": { - "description": "Naci\u015bnij przycisk na mostku, aby zarejestrowa\u0107 Philips Hue z Home Assistant. ", + "description": "Naci\u015bnij przycisk na mostku, aby zarejestrowa\u0107 Philips Hue z Home Assistant.", "title": "Hub Link" } }, diff --git a/homeassistant/components/sensor/.translations/season.vi.json b/homeassistant/components/sensor/.translations/season.vi.json new file mode 100644 index 00000000000..a3bb21dee27 --- /dev/null +++ b/homeassistant/components/sensor/.translations/season.vi.json @@ -0,0 +1,8 @@ +{ + "state": { + "autumn": "M\u00f9a thu", + "spring": "M\u00f9a xu\u00e2n", + "summer": "M\u00f9a h\u00e8", + "winter": "M\u00f9a \u0111\u00f4ng" + } +} \ No newline at end of file From 101b39300b9fcbb01fdc9f237caf37a3a386189a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 23 Mar 2018 14:17:36 -0700 Subject: [PATCH 186/220] Version bump to 0.66.0.beta0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 4ce2f503ad6..b0be1933ffe 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 66 -PATCH_VERSION = '0.dev0' +PATCH_VERSION = '0.beta0' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From 8e14e803cb985943b83339091b8ecb2965cf309a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 23 Mar 2018 14:27:05 -0700 Subject: [PATCH 187/220] Fix release script --- script/release | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/script/release b/script/release index 65a6339cedc..17d5ad9370d 100755 --- a/script/release +++ b/script/release @@ -21,9 +21,9 @@ fi CURRENT_BRANCH=`git rev-parse --abbrev-ref HEAD` -if [ "$CURRENT_BRANCH" != "master" ] +if [ "$CURRENT_BRANCH" != "master" ] && [ "$CURRENT_BRANCH" != "rc" ] then - echo "You have to be on the master branch to release." + echo "You have to be on the master or rc branch to release." exit 1 fi From 8a204fd15bc9217974b6656af93c1eceab8ab499 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 25 Mar 2018 18:10:59 -0700 Subject: [PATCH 188/220] Bump frontend to 20180326.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 9107e64a040..dad07c87cb6 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -24,7 +24,7 @@ from homeassistant.core import callback from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass -REQUIREMENTS = ['home-assistant-frontend==20180322.0'] +REQUIREMENTS = ['home-assistant-frontend==20180326.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index 52833969872..017449bfeca 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -356,7 +356,7 @@ hipnotify==1.0.8 holidays==0.9.4 # homeassistant.components.frontend -home-assistant-frontend==20180322.0 +home-assistant-frontend==20180326.0 # homeassistant.components.homematicip_cloud homematicip==0.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d2c1df2d3bf..fda514af007 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.1 holidays==0.9.4 # homeassistant.components.frontend -home-assistant-frontend==20180322.0 +home-assistant-frontend==20180326.0 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From 0d48a8eec68bcb226df3e8abfafafa40e04ea786 Mon Sep 17 00:00:00 2001 From: Patrick Hofmann Date: Sun, 25 Mar 2018 23:25:28 +0200 Subject: [PATCH 189/220] Security fix & lock for HomeMatic (#11980) * HomeMatic KeyMatic device become a real lock component * Adds supported features to lock component. Locks may are capable to open the door latch. If component is support it, the SUPPORT_OPENING bitmask can be supplied in the supported_features property. * hound improvements. * Travis improvements. * Improvements from review process * Simplifies is_locked method * Adds an openable lock in the lock demo component * removes blank line * Adds test for openable demo lock and lint and reviewer improvements. * adds new line... * Comment end with a period. * Additional blank line. * Mock service based testing, lint fixes * Update description --- .../components/homematic/__init__.py | 9 ++- homeassistant/components/lock/__init__.py | 33 ++++++++++- homeassistant/components/lock/demo.py | 19 +++++- homeassistant/components/lock/homematic.py | 58 +++++++++++++++++++ tests/components/lock/test_demo.py | 12 +++- 5 files changed, 121 insertions(+), 10 deletions(-) create mode 100644 homeassistant/components/lock/homematic.py diff --git a/homeassistant/components/homematic/__init__.py b/homeassistant/components/homematic/__init__.py index 1accf038575..c542cd9e88e 100644 --- a/homeassistant/components/homematic/__init__.py +++ b/homeassistant/components/homematic/__init__.py @@ -33,6 +33,7 @@ DISCOVER_SENSORS = 'homematic.sensor' DISCOVER_BINARY_SENSORS = 'homematic.binary_sensor' DISCOVER_COVER = 'homematic.cover' DISCOVER_CLIMATE = 'homematic.climate' +DISCOVER_LOCKS = 'homematic.locks' ATTR_DISCOVER_DEVICES = 'devices' ATTR_PARAM = 'param' @@ -59,7 +60,7 @@ SERVICE_SET_INSTALL_MODE = 'set_install_mode' HM_DEVICE_TYPES = { DISCOVER_SWITCHES: [ 'Switch', 'SwitchPowermeter', 'IOSwitch', 'IPSwitch', 'RFSiren', - 'IPSwitchPowermeter', 'KeyMatic', 'HMWIOSwitch', 'Rain', 'EcoLogic'], + 'IPSwitchPowermeter', 'HMWIOSwitch', 'Rain', 'EcoLogic'], DISCOVER_LIGHTS: ['Dimmer', 'KeyDimmer', 'IPKeyDimmer'], DISCOVER_SENSORS: [ 'SwitchPowermeter', 'Motion', 'MotionV2', 'RemoteMotion', 'MotionIP', @@ -78,7 +79,8 @@ HM_DEVICE_TYPES = { 'MotionIP', 'RemoteMotion', 'WeatherSensor', 'TiltSensor', 'IPShutterContact', 'HMWIOSwitch', 'MaxShutterContact', 'Rain', 'WiredSensor', 'PresenceIP'], - DISCOVER_COVER: ['Blind', 'KeyBlind', 'IPKeyBlind', 'IPKeyBlindTilt'] + DISCOVER_COVER: ['Blind', 'KeyBlind', 'IPKeyBlind', 'IPKeyBlindTilt'], + DISCOVER_LOCKS: ['KeyMatic'] } HM_IGNORE_DISCOVERY_NODE = [ @@ -464,7 +466,8 @@ def _system_callback_handler(hass, config, src, *args): ('cover', DISCOVER_COVER), ('binary_sensor', DISCOVER_BINARY_SENSORS), ('sensor', DISCOVER_SENSORS), - ('climate', DISCOVER_CLIMATE)): + ('climate', DISCOVER_CLIMATE), + ('lock', DISCOVER_LOCKS)): # Get all devices of a specific type found_devices = _get_devices( hass, discovery_type, addresses, interface) diff --git a/homeassistant/components/lock/__init__.py b/homeassistant/components/lock/__init__.py index d03bbebd696..b3e4ac8f0ff 100644 --- a/homeassistant/components/lock/__init__.py +++ b/homeassistant/components/lock/__init__.py @@ -18,7 +18,7 @@ from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa import homeassistant.helpers.config_validation as cv from homeassistant.const import ( ATTR_CODE, ATTR_CODE_FORMAT, ATTR_ENTITY_ID, STATE_LOCKED, STATE_UNLOCKED, - STATE_UNKNOWN, SERVICE_LOCK, SERVICE_UNLOCK) + STATE_UNKNOWN, SERVICE_LOCK, SERVICE_UNLOCK, SERVICE_OPEN) from homeassistant.components import group ATTR_CHANGED_BY = 'changed_by' @@ -39,6 +39,9 @@ LOCK_SERVICE_SCHEMA = vol.Schema({ vol.Optional(ATTR_CODE): cv.string, }) +# Bitfield of features supported by the lock entity +SUPPORT_OPEN = 1 + _LOGGER = logging.getLogger(__name__) PROP_TO_ATTR = { @@ -78,6 +81,18 @@ def unlock(hass, entity_id=None, code=None): hass.services.call(DOMAIN, SERVICE_UNLOCK, data) +@bind_hass +def open_lock(hass, entity_id=None, code=None): + """Open all or specified locks.""" + data = {} + if code: + data[ATTR_CODE] = code + if entity_id: + data[ATTR_ENTITY_ID] = entity_id + + hass.services.call(DOMAIN, SERVICE_OPEN, data) + + @asyncio.coroutine def async_setup(hass, config): """Track states and offer events for locks.""" @@ -97,6 +112,8 @@ def async_setup(hass, config): for entity in target_locks: if service.service == SERVICE_LOCK: yield from entity.async_lock(code=code) + elif service.service == SERVICE_OPEN: + yield from entity.async_open(code=code) else: yield from entity.async_unlock(code=code) @@ -113,6 +130,9 @@ def async_setup(hass, config): hass.services.async_register( DOMAIN, SERVICE_LOCK, async_handle_lock_service, schema=LOCK_SERVICE_SCHEMA) + hass.services.async_register( + DOMAIN, SERVICE_OPEN, async_handle_lock_service, + schema=LOCK_SERVICE_SCHEMA) return True @@ -158,6 +178,17 @@ class LockDevice(Entity): """ return self.hass.async_add_job(ft.partial(self.unlock, **kwargs)) + def open(self, **kwargs): + """Open the door latch.""" + raise NotImplementedError() + + def async_open(self, **kwargs): + """Open the door latch. + + This method must be run in the event loop and returns a coroutine. + """ + return self.hass.async_add_job(ft.partial(self.open, **kwargs)) + @property def state_attributes(self): """Return the state attributes.""" diff --git a/homeassistant/components/lock/demo.py b/homeassistant/components/lock/demo.py index aca25e7e16d..d561dd333ab 100644 --- a/homeassistant/components/lock/demo.py +++ b/homeassistant/components/lock/demo.py @@ -4,7 +4,7 @@ Demo lock platform that has two fake locks. For more details about this platform, please refer to the documentation https://home-assistant.io/components/demo/ """ -from homeassistant.components.lock import LockDevice +from homeassistant.components.lock import LockDevice, SUPPORT_OPEN from homeassistant.const import (STATE_LOCKED, STATE_UNLOCKED) @@ -13,17 +13,19 @@ def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Demo lock platform.""" add_devices([ DemoLock('Front Door', STATE_LOCKED), - DemoLock('Kitchen Door', STATE_UNLOCKED) + DemoLock('Kitchen Door', STATE_UNLOCKED), + DemoLock('Openable Lock', STATE_LOCKED, True) ]) class DemoLock(LockDevice): """Representation of a Demo lock.""" - def __init__(self, name, state): + def __init__(self, name, state, openable=False): """Initialize the lock.""" self._name = name self._state = state + self._openable = openable @property def should_poll(self): @@ -49,3 +51,14 @@ class DemoLock(LockDevice): """Unlock the device.""" self._state = STATE_UNLOCKED self.schedule_update_ha_state() + + def open(self, **kwargs): + """Open the door latch.""" + self._state = STATE_UNLOCKED + self.schedule_update_ha_state() + + @property + def supported_features(self): + """Flag supported features.""" + if self._openable: + return SUPPORT_OPEN diff --git a/homeassistant/components/lock/homematic.py b/homeassistant/components/lock/homematic.py new file mode 100644 index 00000000000..0d70849e37e --- /dev/null +++ b/homeassistant/components/lock/homematic.py @@ -0,0 +1,58 @@ +""" +Support for Homematic lock. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/lock.homematic/ +""" +import logging +from homeassistant.components.lock import LockDevice, SUPPORT_OPEN +from homeassistant.components.homematic import HMDevice, ATTR_DISCOVER_DEVICES +from homeassistant.const import STATE_UNKNOWN + + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['homematic'] + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the Homematic lock platform.""" + if discovery_info is None: + return + + devices = [] + for conf in discovery_info[ATTR_DISCOVER_DEVICES]: + devices.append(HMLock(conf)) + + add_devices(devices) + + +class HMLock(HMDevice, LockDevice): + """Representation of a Homematic lock aka KeyMatic.""" + + @property + def is_locked(self): + """Return true if the lock is locked.""" + return not bool(self._hm_get_state()) + + def lock(self, **kwargs): + """Lock the lock.""" + self._hmdevice.lock() + + def unlock(self, **kwargs): + """Unlock the lock.""" + self._hmdevice.unlock() + + def open(self, **kwargs): + """Open the door latch.""" + self._hmdevice.open() + + def _init_data_struct(self): + """Generate the data dictionary (self._data) from metadata.""" + self._state = "STATE" + self._data.update({self._state: STATE_UNKNOWN}) + + @property + def supported_features(self): + """Flag supported features.""" + return SUPPORT_OPEN diff --git a/tests/components/lock/test_demo.py b/tests/components/lock/test_demo.py index 12007d2b8ad..1d774248f35 100644 --- a/tests/components/lock/test_demo.py +++ b/tests/components/lock/test_demo.py @@ -4,11 +4,10 @@ import unittest from homeassistant.setup import setup_component from homeassistant.components import lock -from tests.common import get_test_home_assistant - - +from tests.common import get_test_home_assistant, mock_service FRONT = 'lock.front_door' KITCHEN = 'lock.kitchen_door' +OPENABLE_LOCK = 'lock.openable_lock' class TestLockDemo(unittest.TestCase): @@ -48,3 +47,10 @@ class TestLockDemo(unittest.TestCase): self.hass.block_till_done() self.assertFalse(lock.is_locked(self.hass, FRONT)) + + def test_opening(self): + """Test the opening of a lock.""" + calls = mock_service(self.hass, lock.DOMAIN, lock.SERVICE_OPEN) + lock.open_lock(self.hass, OPENABLE_LOCK) + self.hass.block_till_done() + self.assertEqual(1, len(calls)) From a08293cff7facd2a204f5fd8e84aff0db9d43cd4 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Sun, 25 Mar 2018 01:12:26 +0100 Subject: [PATCH 190/220] Log invalid templates in script delays (#13423) * Log invalid templates in script delays * Abort on error * Remove unused import --- homeassistant/helpers/script.py | 15 ++++++++++----- tests/helpers/test_script.py | 26 ++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 5 deletions(-) diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index abfdde8c8e1..f2ae36e7fd0 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -97,11 +97,16 @@ class Script(): delay = action[CONF_DELAY] - if isinstance(delay, template.Template): - delay = vol.All( - cv.time_period, - cv.positive_timedelta)( - delay.async_render(variables)) + try: + if isinstance(delay, template.Template): + delay = vol.All( + cv.time_period, + cv.positive_timedelta)( + delay.async_render(variables)) + except (TemplateError, vol.Invalid) as ex: + _LOGGER.error("Error rendering '%s' delay template: %s", + self.name, ex) + break unsub = async_track_point_in_utc_time( self.hass, async_script_delay, diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index a8ae20ad69b..4297ca26e7d 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -218,6 +218,32 @@ class TestScriptHelper(unittest.TestCase): assert not script_obj.is_running assert len(events) == 2 + def test_delay_invalid_template(self): + """Test the delay as a template that fails.""" + event = 'test_event' + events = [] + + @callback + def record_event(event): + """Add recorded event to set.""" + events.append(event) + + self.hass.bus.listen(event, record_event) + + script_obj = script.Script(self.hass, cv.SCRIPT_SCHEMA([ + {'event': event}, + {'delay': '{{ invalid_delay }}'}, + {'delay': {'seconds': 5}}, + {'event': event}])) + + with mock.patch.object(script, '_LOGGER') as mock_logger: + script_obj.run() + self.hass.block_till_done() + assert mock_logger.error.called + + assert not script_obj.is_running + assert len(events) == 1 + def test_cancel_while_delay(self): """Test the cancelling while the delay is present.""" event = 'test_event' From 444805df103619f9aa52591a11ff1d4fb6e224b0 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Sun, 25 Mar 2018 09:47:10 +0200 Subject: [PATCH 191/220] LimitlessLED hs_color fixes (#13425) --- homeassistant/components/light/limitlessled.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/light/limitlessled.py b/homeassistant/components/light/limitlessled.py index 5a6a0a34959..bb84b3a6fed 100644 --- a/homeassistant/components/light/limitlessled.py +++ b/homeassistant/components/light/limitlessled.py @@ -197,7 +197,7 @@ class LimitlessLEDGroup(Light): self._is_on = (last_state.state == STATE_ON) self._brightness = last_state.attributes.get('brightness') self._temperature = last_state.attributes.get('color_temp') - self._color = last_state.attributes.get('rgb_color') + self._color = last_state.attributes.get('hs_color') @property def should_poll(self): @@ -336,4 +336,4 @@ class LimitlessLEDGroup(Light): def limitlessled_color(self): """Convert Home Assistant HS list to RGB Color tuple.""" from limitlessled import Color - return Color(color_hs_to_RGB(*tuple(self._color))) + return Color(*color_hs_to_RGB(*tuple(self._color))) From 24d299e266833a2d22369500bb41d91c058f333d Mon Sep 17 00:00:00 2001 From: a-andre Date: Mon, 26 Mar 2018 03:07:26 +0200 Subject: [PATCH 192/220] Hyperion: fix typo (#13429) --- homeassistant/components/light/hyperion.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/light/hyperion.py b/homeassistant/components/light/hyperion.py index e5a4bd18115..8ba2329af7e 100644 --- a/homeassistant/components/light/hyperion.py +++ b/homeassistant/components/light/hyperion.py @@ -215,7 +215,7 @@ class Hyperion(Light): pass led_color = response['info']['activeLedColor'] - if not led_color or led_color[0]['RGB value'] == [0, 0, 0]: + if not led_color or led_color[0]['RGB Value'] == [0, 0, 0]: # Get the active effect if response['info'].get('activeEffects'): self._rgb_color = [175, 0, 255] @@ -234,8 +234,7 @@ class Hyperion(Light): self._effect = None else: # Get the RGB color - self._rgb_color =\ - response['info']['activeLedColor'][0]['RGB Value'] + self._rgb_color = led_color[0]['RGB Value'] self._brightness = max(self._rgb_color) self._rgb_mem = [int(round(float(x)*255/self._brightness)) for x in self._rgb_color] From 60f6109cbfdfb180a853b8dd6cadc6c7503d8c9e Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Sun, 25 Mar 2018 12:53:15 +0200 Subject: [PATCH 193/220] HomeKit: Bugfix & improved logging (#13431) * Bugfix & improved logging * Removed logging statements * Removed logging test --- homeassistant/components/homekit/__init__.py | 4 ---- homeassistant/components/homekit/type_covers.py | 1 + homeassistant/components/homekit/type_lights.py | 4 ++++ .../components/homekit/type_security_systems.py | 1 + homeassistant/components/homekit/type_switches.py | 1 + homeassistant/components/homekit/type_thermostats.py | 4 ++++ tests/components/homekit/test_get_accessories.py | 11 ----------- 7 files changed, 11 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 02449607bf2..4854a828e41 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -73,8 +73,6 @@ async def async_setup(hass, config): def get_accessory(hass, state, aid, config): """Take state and return an accessory object if supported.""" - _LOGGER.debug('', - state.entity_id, aid, config) if not aid: _LOGGER.warning('The entitiy "%s" is not supported, since it ' 'generates an invalid aid, please change it.', @@ -129,8 +127,6 @@ def get_accessory(hass, state, aid, config): _LOGGER.debug('Add "%s" as "%s"', state.entity_id, 'Switch') return TYPES['Switch'](hass, state.entity_id, state.name, aid=aid) - _LOGGER.warning('The entity "%s" is not supported yet', - state.entity_id) return None diff --git a/homeassistant/components/homekit/type_covers.py b/homeassistant/components/homekit/type_covers.py index 36cfa4d635a..7616ef05fdf 100644 --- a/homeassistant/components/homekit/type_covers.py +++ b/homeassistant/components/homekit/type_covers.py @@ -46,6 +46,7 @@ class WindowCovering(HomeAccessory): def move_cover(self, value): """Move cover to value if call came from HomeKit.""" + self.char_target_position.set_value(value, should_callback=False) if value != self.current_position: _LOGGER.debug('%s: Set position to %d', self._entity_id, value) self.homekit_target = value diff --git a/homeassistant/components/homekit/type_lights.py b/homeassistant/components/homekit/type_lights.py index c723fcc08a6..2415bb1a4df 100644 --- a/homeassistant/components/homekit/type_lights.py +++ b/homeassistant/components/homekit/type_lights.py @@ -71,6 +71,7 @@ class Light(HomeAccessory): _LOGGER.debug('%s: Set state to %d', self._entity_id, value) self._flag[CHAR_ON] = True + self.char_on.set_value(value, should_callback=False) if value == 1: self._hass.components.light.turn_on(self._entity_id) @@ -81,6 +82,7 @@ class Light(HomeAccessory): """Set brightness if call came from HomeKit.""" _LOGGER.debug('%s: Set brightness to %d', self._entity_id, value) self._flag[CHAR_BRIGHTNESS] = True + self.char_brightness.set_value(value, should_callback=False) self._hass.components.light.turn_on( self._entity_id, brightness_pct=value) @@ -88,6 +90,7 @@ class Light(HomeAccessory): """Set saturation if call came from HomeKit.""" _LOGGER.debug('%s: Set saturation to %d', self._entity_id, value) self._flag[CHAR_SATURATION] = True + self.char_saturation.set_value(value, should_callback=False) self._saturation = value self.set_color() @@ -95,6 +98,7 @@ class Light(HomeAccessory): """Set hue if call came from HomeKit.""" _LOGGER.debug('%s: Set hue to %d', self._entity_id, value) self._flag[CHAR_HUE] = True + self.char_hue.set_value(value, should_callback=False) self._hue = value self.set_color() diff --git a/homeassistant/components/homekit/type_security_systems.py b/homeassistant/components/homekit/type_security_systems.py index 1d47160f9d2..146fca95b53 100644 --- a/homeassistant/components/homekit/type_security_systems.py +++ b/homeassistant/components/homekit/type_security_systems.py @@ -54,6 +54,7 @@ class SecuritySystem(HomeAccessory): _LOGGER.debug('%s: Set security state to %d', self._entity_id, value) self.flag_target_state = True + self.char_target_state.set_value(value, should_callback=False) hass_value = HOMEKIT_TO_HASS[value] service = STATE_TO_SERVICE[hass_value] diff --git a/homeassistant/components/homekit/type_switches.py b/homeassistant/components/homekit/type_switches.py index fd3291ffe23..1f19893d0be 100644 --- a/homeassistant/components/homekit/type_switches.py +++ b/homeassistant/components/homekit/type_switches.py @@ -36,6 +36,7 @@ class Switch(HomeAccessory): _LOGGER.debug('%s: Set switch state to %s', self._entity_id, value) self.flag_target_state = True + self.char_on.set_value(value, should_callback=False) service = SERVICE_TURN_ON if value else SERVICE_TURN_OFF self._hass.services.call(self._domain, service, {ATTR_ENTITY_ID: self._entity_id}) diff --git a/homeassistant/components/homekit/type_thermostats.py b/homeassistant/components/homekit/type_thermostats.py index b73b492ba74..3f545e90eb3 100644 --- a/homeassistant/components/homekit/type_thermostats.py +++ b/homeassistant/components/homekit/type_thermostats.py @@ -97,6 +97,7 @@ class Thermostat(HomeAccessory): def set_heat_cool(self, value): """Move operation mode to value if call came from HomeKit.""" + self.char_target_heat_cool.set_value(value, should_callback=False) if value in HC_HOMEKIT_TO_HASS: _LOGGER.debug('%s: Set heat-cool to %d', self._entity_id, value) self.heat_cool_flag_target_state = True @@ -109,6 +110,7 @@ class Thermostat(HomeAccessory): _LOGGER.debug('%s: Set cooling threshold temperature to %.2f', self._entity_id, value) self.coolingthresh_flag_target_state = True + self.char_cooling_thresh_temp.set_value(value, should_callback=False) low = self.char_heating_thresh_temp.value self._hass.components.climate.set_temperature( entity_id=self._entity_id, target_temp_high=value, @@ -119,6 +121,7 @@ class Thermostat(HomeAccessory): _LOGGER.debug('%s: Set heating threshold temperature to %.2f', self._entity_id, value) self.heatingthresh_flag_target_state = True + self.char_heating_thresh_temp.set_value(value, should_callback=False) # Home assistant always wants to set low and high at the same time high = self.char_cooling_thresh_temp.value self._hass.components.climate.set_temperature( @@ -130,6 +133,7 @@ class Thermostat(HomeAccessory): _LOGGER.debug('%s: Set target temperature to %.2f', self._entity_id, value) self.temperature_flag_target_state = True + self.char_target_temp.set_value(value, should_callback=False) self._hass.components.climate.set_temperature( temperature=value, entity_id=self._entity_id) diff --git a/tests/components/homekit/test_get_accessories.py b/tests/components/homekit/test_get_accessories.py index e6dbe1ff729..ee7baae2755 100644 --- a/tests/components/homekit/test_get_accessories.py +++ b/tests/components/homekit/test_get_accessories.py @@ -16,17 +16,6 @@ _LOGGER = logging.getLogger(__name__) CONFIG = {} -def test_get_accessory_invalid(caplog): - """Test with unsupported component.""" - assert get_accessory(None, State('test.unsupported', 'on'), 2, None) \ - is None - assert caplog.records[1].levelname == 'WARNING' - - assert get_accessory(None, State('test.test', 'on'), None, None) \ - is None - assert caplog.records[3].levelname == 'WARNING' - - class TestGetAccessories(unittest.TestCase): """Methods to test the get_accessory method.""" From 22cefc7e640bee867296a98c9098470f52aa052b Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Sun, 25 Mar 2018 12:51:11 +0200 Subject: [PATCH 194/220] Improve detection of entity names in templates (#13432) * Improve detection of entity names in templates * Only test variables --- homeassistant/helpers/template.py | 5 +++-- tests/helpers/test_template.py | 6 ++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 3dd65aa362c..28ab4e9bfa0 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -13,7 +13,7 @@ from jinja2.sandbox import ImmutableSandboxedEnvironment from homeassistant.const import ( ATTR_LATITUDE, ATTR_LONGITUDE, ATTR_UNIT_OF_MEASUREMENT, MATCH_ALL, STATE_UNKNOWN) -from homeassistant.core import State +from homeassistant.core import State, valid_entity_id from homeassistant.exceptions import TemplateError from homeassistant.helpers import location as loc_helper from homeassistant.loader import bind_hass, get_component @@ -73,7 +73,8 @@ def extract_entities(template, variables=None): extraction_final.append(result[0]) if variables and result[1] in variables and \ - isinstance(variables[result[1]], str): + isinstance(variables[result[1]], str) and \ + valid_entity_id(variables[result[1]]): extraction_final.append(variables[result[1]]) if extraction_final: diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index 47e46bae3c7..def06ea9284 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -836,6 +836,12 @@ is_state_attr('device_tracker.phone_2', 'battery', 40) "{{ is_state(trigger.entity_id, 'off') }}", {'trigger': {'entity_id': 'input_boolean.switch'}})) + self.assertEqual( + MATCH_ALL, + template.extract_entities( + "{{ is_state('media_player.' ~ where , 'playing') }}", + {'where': 'livingroom'})) + @asyncio.coroutine def test_state_with_unit(hass): From 93b9ec0b0f904b0f20b5c1b8b922e68806196de9 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 25 Mar 2018 18:04:20 -0700 Subject: [PATCH 195/220] Add version bump script (#13447) * Add version bump script * Lint --- script/version_bump.py | 135 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100755 script/version_bump.py diff --git a/script/version_bump.py b/script/version_bump.py new file mode 100755 index 00000000000..0cd02ddbfcb --- /dev/null +++ b/script/version_bump.py @@ -0,0 +1,135 @@ +#!/usr/bin/env python3 +"""Helper script to bump the current version.""" +import argparse +import re + +from homeassistant import const + + +PARSE_PATCH = r'(?P\d+)(\.(?P\D+)(?P\d+))?' + + +def format_patch(patch_parts): + """Format the patch parts back into a patch string.""" + return '{patch}.{prerel}{prerelversion}'.format(**patch_parts) + + +def bump_version(cur_major, cur_minor, cur_patch, bump_type): + """Return a new version given a current version and action.""" + patch_parts = re.match(PARSE_PATCH, cur_patch).groupdict() + patch_parts['patch'] = int(patch_parts['patch']) + if patch_parts['prerelversion'] is not None: + patch_parts['prerelversion'] = int(patch_parts['prerelversion']) + + if bump_type == 'release_patch': + # Convert 0.67.3 to 0.67.4 + # Convert 0.67.3.beta5 to 0.67.3 + # Convert 0.67.3.dev0 to 0.67.3 + new_major = cur_major + new_minor = cur_minor + + if patch_parts['prerel'] is None: + new_patch = str(patch_parts['patch'] + 1) + else: + new_patch = str(patch_parts['patch']) + + elif bump_type == 'dev': + # Convert 0.67.3 to 0.67.4.dev0 + # Convert 0.67.3.beta5 to 0.67.4.dev0 + # Convert 0.67.3.dev0 to 0.67.3.dev1 + new_major = cur_major + + if patch_parts['prerel'] == 'dev': + new_minor = cur_minor + patch_parts['prerelversion'] += 1 + new_patch = format_patch(patch_parts) + else: + new_minor = cur_minor + 1 + new_patch = '0.dev0' + + elif bump_type == 'beta': + # Convert 0.67.5 to 0.67.8.beta0 + # Convert 0.67.0.dev0 to 0.67.0.beta0 + # Convert 0.67.5.beta4 to 0.67.5.beta5 + new_major = cur_major + new_minor = cur_minor + + if patch_parts['prerel'] is None: + patch_parts['patch'] += 1 + patch_parts['prerel'] = 'beta' + patch_parts['prerelversion'] = 0 + + elif patch_parts['prerel'] == 'beta': + patch_parts['prerelversion'] += 1 + + elif patch_parts['prerel'] == 'dev': + patch_parts['prerel'] = 'beta' + patch_parts['prerelversion'] = 0 + + else: + raise Exception('Can only bump from beta or no prerel version') + + new_patch = format_patch(patch_parts) + + return new_major, new_minor, new_patch + + +def write_version(major, minor, patch): + """Update Home Assistant constant file with new version.""" + with open('homeassistant/const.py') as fil: + content = fil.read() + + content = re.sub('MAJOR_VERSION = .*\n', + 'MAJOR_VERSION = {}\n'.format(major), + content) + content = re.sub('MINOR_VERSION = .*\n', + 'MINOR_VERSION = {}\n'.format(minor), + content) + content = re.sub('PATCH_VERSION = .*\n', + "PATCH_VERSION = '{}'\n".format(patch), + content) + + with open('homeassistant/const.py', 'wt') as fil: + content = fil.write(content) + + +def main(): + """Execute script.""" + parser = argparse.ArgumentParser( + description="Bump version of Home Assistant") + parser.add_argument( + 'type', + help="The type of the bump the version to.", + choices=['beta', 'dev', 'release_patch'], + ) + arguments = parser.parse_args() + write_version(*bump_version(const.MAJOR_VERSION, const.MINOR_VERSION, + const.PATCH_VERSION, arguments.type)) + + +def test_bump_version(): + """Make sure it all works.""" + assert bump_version(0, 56, '0', 'beta') == \ + (0, 56, '1.beta0') + assert bump_version(0, 56, '0.beta3', 'beta') == \ + (0, 56, '0.beta4') + assert bump_version(0, 56, '0.dev0', 'beta') == \ + (0, 56, '0.beta0') + + assert bump_version(0, 56, '3', 'dev') == \ + (0, 57, '0.dev0') + assert bump_version(0, 56, '0.beta3', 'dev') == \ + (0, 57, '0.dev0') + assert bump_version(0, 56, '0.dev0', 'dev') == \ + (0, 56, '0.dev1') + + assert bump_version(0, 56, '3', 'release_patch') == \ + (0, 56, '4') + assert bump_version(0, 56, '3.beta3', 'release_patch') == \ + (0, 56, '3') + assert bump_version(0, 56, '0.dev0', 'release_patch') == \ + (0, 56, '0') + + +if __name__ == '__main__': + main() From 38d2702e3c05d7df42f87c998b8b819e6a1e3c40 Mon Sep 17 00:00:00 2001 From: Cedric Van Goethem Date: Mon, 26 Mar 2018 02:03:23 +0100 Subject: [PATCH 196/220] Add extra check for ESSID field in case there's a wired connection (#13459) --- homeassistant/components/device_tracker/unifi.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/device_tracker/unifi.py b/homeassistant/components/device_tracker/unifi.py index 8663930c4e6..d8a52aaaeb4 100644 --- a/homeassistant/components/device_tracker/unifi.py +++ b/homeassistant/components/device_tracker/unifi.py @@ -98,7 +98,8 @@ class UnifiScanner(DeviceScanner): # Filter clients to provided SSID list if self._ssid_filter: clients = [client for client in clients - if client['essid'] in self._ssid_filter] + if 'essid' in client and + client['essid'] in self._ssid_filter] self._clients = { client['mac']: client From 068b037944ab3ee918430e0607294cadaf521b1f Mon Sep 17 00:00:00 2001 From: Beat <508289+bdurrer@users.noreply.github.com> Date: Mon, 26 Mar 2018 03:02:21 +0200 Subject: [PATCH 197/220] Fix encoding errors in mikrotik device tracker (#13464) --- homeassistant/components/device_tracker/mikrotik.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/device_tracker/mikrotik.py b/homeassistant/components/device_tracker/mikrotik.py index 1d9161c0d45..154fc3d2a63 100644 --- a/homeassistant/components/device_tracker/mikrotik.py +++ b/homeassistant/components/device_tracker/mikrotik.py @@ -73,7 +73,8 @@ class MikrotikScanner(DeviceScanner): self.host, self.username, self.password, - port=int(self.port) + port=int(self.port), + encoding='utf-8' ) try: From a507ed0af8aaa4b6a464cdb9b138f4a8173c404b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 25 Mar 2018 18:24:16 -0700 Subject: [PATCH 198/220] Version bump to 0.66.0.beta1 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index b0be1933ffe..382323ed534 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 66 -PATCH_VERSION = '0.beta0' +PATCH_VERSION = '0.beta1' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From dfe3219f3f39254b30468dc273285ce936c952e3 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 26 Mar 2018 14:00:56 -0700 Subject: [PATCH 199/220] Hue: Convert XY to HS color if HS not present (#13465) * Hue: Convert XY to HS color if HS not present * Revert change to test * Address comments * Lint --- homeassistant/components/light/hue.py | 30 ++++++++------ tests/components/light/test_hue.py | 57 +++++++++++++++++++++++++++ 2 files changed, 76 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/light/hue.py b/homeassistant/components/light/hue.py index 71e3d4fa30b..4a54f0a337d 100644 --- a/homeassistant/components/light/hue.py +++ b/homeassistant/components/light/hue.py @@ -18,6 +18,7 @@ from homeassistant.components.light import ( FLASH_LONG, FLASH_SHORT, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, SUPPORT_FLASH, SUPPORT_COLOR, SUPPORT_TRANSITION, Light) +from homeassistant.util import color DEPENDENCIES = ['hue'] SCAN_INTERVAL = timedelta(seconds=5) @@ -235,19 +236,26 @@ class HueLight(Light): @property def hs_color(self): """Return the hs color value.""" - # Don't return hue/sat if in color temperature mode - if self._color_mode == "ct": + # pylint: disable=redefined-outer-name + mode = self._color_mode + + if mode not in ('hs', 'xy'): + return + + source = self.light.action if self.is_group else self.light.state + + hue = source.get('hue') + sat = source.get('sat') + + # Sometimes the state will not include valid hue/sat values. + # Reported as issue 13434 + if hue is not None and sat is not None: + return hue / 65535 * 360, sat / 255 * 100 + + if 'xy' not in source: return None - if self.is_group: - return ( - self.light.action.get('hue') / 65535 * 360, - self.light.action.get('sat') / 255 * 100, - ) - return ( - self.light.state.get('hue') / 65535 * 360, - self.light.state.get('sat') / 255 * 100, - ) + return color.color_xy_to_hs(*source['xy']) @property def color_temp(self): diff --git a/tests/components/light/test_hue.py b/tests/components/light/test_hue.py index 54bb2184a64..d73531b1b9a 100644 --- a/tests/components/light/test_hue.py +++ b/tests/components/light/test_hue.py @@ -11,6 +11,7 @@ import pytest from homeassistant.components import hue import homeassistant.components.light.hue as hue_light +from homeassistant.util import color _LOGGER = logging.getLogger(__name__) @@ -623,3 +624,59 @@ def test_available(): ) assert light.available is True + + +def test_hs_color(): + """Test hs_color property.""" + light = hue_light.HueLight( + light=Mock(state={ + 'colormode': 'ct', + 'hue': 1234, + 'sat': 123, + }), + request_bridge_update=None, + bridge=Mock(), + is_group=False, + ) + + assert light.hs_color is None + + light = hue_light.HueLight( + light=Mock(state={ + 'colormode': 'hs', + 'hue': 1234, + 'sat': 123, + }), + request_bridge_update=None, + bridge=Mock(), + is_group=False, + ) + + assert light.hs_color == (1234 / 65535 * 360, 123 / 255 * 100) + + light = hue_light.HueLight( + light=Mock(state={ + 'colormode': 'xy', + 'hue': 1234, + 'sat': 123, + }), + request_bridge_update=None, + bridge=Mock(), + is_group=False, + ) + + assert light.hs_color == (1234 / 65535 * 360, 123 / 255 * 100) + + light = hue_light.HueLight( + light=Mock(state={ + 'colormode': 'xy', + 'hue': None, + 'sat': 123, + 'xy': [0.4, 0.5] + }), + request_bridge_update=None, + bridge=Mock(), + is_group=False, + ) + + assert light.hs_color == color.color_xy_to_hs(0.4, 0.5) From f48ce3d437c5bcfcbd4eb4d2ff6ca7756dd93966 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Tue, 27 Mar 2018 01:08:44 +0200 Subject: [PATCH 200/220] Fix ID (fixes #13444) (#13471) --- homeassistant/components/media_player/mpchc.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/media_player/mpchc.py b/homeassistant/components/media_player/mpchc.py index cc195db2590..a375a585ad4 100644 --- a/homeassistant/components/media_player/mpchc.py +++ b/homeassistant/components/media_player/mpchc.py @@ -155,8 +155,8 @@ class MpcHcDevice(MediaPlayerDevice): def media_next_track(self): """Send next track command.""" - self._send_command(921) + self._send_command(920) def media_previous_track(self): """Send previous track command.""" - self._send_command(920) + self._send_command(919) From ce3a5972c787b1cc854d0ae1deb3558b28a816f7 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 26 Mar 2018 16:07:22 -0700 Subject: [PATCH 201/220] Upgrade aiohue and fix race condition (#13475) * Bump aiohue to 1.3 * Store bridge in hass.data before setting up platform * Fix tests --- homeassistant/components/hue/__init__.py | 5 +++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/hue/test_bridge.py | 1 + tests/components/hue/test_setup.py | 6 +----- 5 files changed, 7 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/hue/__init__.py b/homeassistant/components/hue/__init__.py index 2fb55f8f6e0..b70021e0304 100644 --- a/homeassistant/components/hue/__init__.py +++ b/homeassistant/components/hue/__init__.py @@ -21,7 +21,7 @@ from homeassistant.helpers import discovery, aiohttp_client from homeassistant import config_entries from homeassistant.util.json import save_json -REQUIREMENTS = ['aiohue==1.2.0'] +REQUIREMENTS = ['aiohue==1.3.0'] _LOGGER = logging.getLogger(__name__) @@ -145,7 +145,6 @@ async def async_setup_bridge( bridge = HueBridge(host, hass, filename, username, allow_unreachable, allow_hue_groups) await bridge.async_setup() - hass.data[DOMAIN][host] = bridge def _find_username_from_config(hass, filename): @@ -209,6 +208,8 @@ class HueBridge(object): self.host) return + self.hass.data[DOMAIN][self.host] = self + # If we came here and configuring this host, mark as done if self.config_request_id: request_id = self.config_request_id diff --git a/requirements_all.txt b/requirements_all.txt index 017449bfeca..e30b70eb976 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -74,7 +74,7 @@ aiodns==1.1.1 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==1.2.0 +aiohue==1.3.0 # homeassistant.components.sensor.imap aioimaplib==0.7.13 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fda514af007..27d3bd21ad7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -35,7 +35,7 @@ aioautomatic==0.6.5 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==1.2.0 +aiohue==1.3.0 # homeassistant.components.notify.apns apns2==0.3.0 diff --git a/tests/components/hue/test_bridge.py b/tests/components/hue/test_bridge.py index 88a7223d91e..39351699df5 100644 --- a/tests/components/hue/test_bridge.py +++ b/tests/components/hue/test_bridge.py @@ -66,6 +66,7 @@ async def test_only_create_no_username(hass): async def test_configurator_callback(hass, mock_request): """.""" + hass.data[hue.DOMAIN] = {} with patch('aiohue.Bridge.create_user', side_effect=aiohue.LinkButtonNotPressed): await MockBridge(hass).async_setup() diff --git a/tests/components/hue/test_setup.py b/tests/components/hue/test_setup.py index 690419fcb7a..f90f58a50c3 100644 --- a/tests/components/hue/test_setup.py +++ b/tests/components/hue/test_setup.py @@ -18,7 +18,6 @@ async def test_setup_with_multiple_hosts(hass, mock_bridge): assert len(mock_bridge.mock_calls) == 2 hosts = sorted(mock_call[1][0] for mock_call in mock_bridge.mock_calls) assert hosts == ['127.0.0.1', '192.168.1.10'] - assert len(hass.data[hue.DOMAIN]) == 2 async def test_bridge_discovered(hass, mock_bridge): @@ -33,7 +32,6 @@ async def test_bridge_discovered(hass, mock_bridge): assert len(mock_bridge.mock_calls) == 1 assert mock_bridge.mock_calls[0][1][0] == '192.168.1.10' - assert len(hass.data[hue.DOMAIN]) == 1 async def test_bridge_configure_and_discovered(hass, mock_bridge): @@ -48,7 +46,7 @@ async def test_bridge_configure_and_discovered(hass, mock_bridge): assert len(mock_bridge.mock_calls) == 1 assert mock_bridge.mock_calls[0][1][0] == '192.168.1.10' - assert len(hass.data[hue.DOMAIN]) == 1 + hass.data[hue.DOMAIN] = {'192.168.1.10': {}} mock_bridge.reset_mock() @@ -59,7 +57,6 @@ async def test_bridge_configure_and_discovered(hass, mock_bridge): await hass.async_block_till_done() assert len(mock_bridge.mock_calls) == 0 - assert len(hass.data[hue.DOMAIN]) == 1 async def test_setup_no_host(hass, aioclient_mock): @@ -71,4 +68,3 @@ async def test_setup_no_host(hass, aioclient_mock): assert result assert len(aioclient_mock.mock_calls) == 1 - assert len(hass.data[hue.DOMAIN]) == 0 From a06eea444af977c8513db2fe7c1593da816bec0e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 26 Mar 2018 14:55:09 -0700 Subject: [PATCH 202/220] version should contain just 'b' not 'beta' (#13476) --- script/version_bump.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/script/version_bump.py b/script/version_bump.py index 0cd02ddbfcb..0500fc45957 100755 --- a/script/version_bump.py +++ b/script/version_bump.py @@ -23,7 +23,7 @@ def bump_version(cur_major, cur_minor, cur_patch, bump_type): if bump_type == 'release_patch': # Convert 0.67.3 to 0.67.4 - # Convert 0.67.3.beta5 to 0.67.3 + # Convert 0.67.3.b5 to 0.67.3 # Convert 0.67.3.dev0 to 0.67.3 new_major = cur_major new_minor = cur_minor @@ -35,7 +35,7 @@ def bump_version(cur_major, cur_minor, cur_patch, bump_type): elif bump_type == 'dev': # Convert 0.67.3 to 0.67.4.dev0 - # Convert 0.67.3.beta5 to 0.67.4.dev0 + # Convert 0.67.3.b5 to 0.67.4.dev0 # Convert 0.67.3.dev0 to 0.67.3.dev1 new_major = cur_major @@ -48,22 +48,22 @@ def bump_version(cur_major, cur_minor, cur_patch, bump_type): new_patch = '0.dev0' elif bump_type == 'beta': - # Convert 0.67.5 to 0.67.8.beta0 - # Convert 0.67.0.dev0 to 0.67.0.beta0 - # Convert 0.67.5.beta4 to 0.67.5.beta5 + # Convert 0.67.5 to 0.67.8.b0 + # Convert 0.67.0.dev0 to 0.67.0.b0 + # Convert 0.67.5.b4 to 0.67.5.b5 new_major = cur_major new_minor = cur_minor if patch_parts['prerel'] is None: patch_parts['patch'] += 1 - patch_parts['prerel'] = 'beta' + patch_parts['prerel'] = 'b' patch_parts['prerelversion'] = 0 - elif patch_parts['prerel'] == 'beta': + elif patch_parts['prerel'] == 'b': patch_parts['prerelversion'] += 1 elif patch_parts['prerel'] == 'dev': - patch_parts['prerel'] = 'beta' + patch_parts['prerel'] = 'b' patch_parts['prerelversion'] = 0 else: @@ -110,22 +110,22 @@ def main(): def test_bump_version(): """Make sure it all works.""" assert bump_version(0, 56, '0', 'beta') == \ - (0, 56, '1.beta0') - assert bump_version(0, 56, '0.beta3', 'beta') == \ - (0, 56, '0.beta4') + (0, 56, '1.b0') + assert bump_version(0, 56, '0.b3', 'beta') == \ + (0, 56, '0.b4') assert bump_version(0, 56, '0.dev0', 'beta') == \ - (0, 56, '0.beta0') + (0, 56, '0.b0') assert bump_version(0, 56, '3', 'dev') == \ (0, 57, '0.dev0') - assert bump_version(0, 56, '0.beta3', 'dev') == \ + assert bump_version(0, 56, '0.b3', 'dev') == \ (0, 57, '0.dev0') assert bump_version(0, 56, '0.dev0', 'dev') == \ (0, 56, '0.dev1') assert bump_version(0, 56, '3', 'release_patch') == \ (0, 56, '4') - assert bump_version(0, 56, '3.beta3', 'release_patch') == \ + assert bump_version(0, 56, '3.b3', 'release_patch') == \ (0, 56, '3') assert bump_version(0, 56, '0.dev0', 'release_patch') == \ (0, 56, '0') From 94d9aa0c5fe023725d74beb6a61402f3b1169a3b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 26 Mar 2018 16:10:03 -0700 Subject: [PATCH 203/220] Bump version to 0.66.0.b2 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 382323ed534..58c49289989 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 66 -PATCH_VERSION = '0.beta1' +PATCH_VERSION = '0.b2' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From 3639a4470c6c626414a8c4018f455e3f79b59170 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 26 Mar 2018 16:16:42 -0700 Subject: [PATCH 204/220] Use twine for release --- script/release | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/script/release b/script/release index 17d5ad9370d..dc3e208bc1a 100755 --- a/script/release +++ b/script/release @@ -27,4 +27,5 @@ then exit 1 fi -python3 setup.py sdist bdist_wheel upload +python3 setup.py sdist bdist_wheel +python3 -m twine upload dist/* --skip-existing From 27865f58f11774a74f5a014e8c54feedb180ef30 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 29 Mar 2018 17:00:16 -0700 Subject: [PATCH 205/220] Bump frontend to 20180330.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 dad07c87cb6..b2f50148bd3 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -24,7 +24,7 @@ from homeassistant.core import callback from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass -REQUIREMENTS = ['home-assistant-frontend==20180326.0'] +REQUIREMENTS = ['home-assistant-frontend==20180330.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index e30b70eb976..df81dae9f00 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -356,7 +356,7 @@ hipnotify==1.0.8 holidays==0.9.4 # homeassistant.components.frontend -home-assistant-frontend==20180326.0 +home-assistant-frontend==20180330.0 # homeassistant.components.homematicip_cloud homematicip==0.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 27d3bd21ad7..33527a913a5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.1 holidays==0.9.4 # homeassistant.components.frontend -home-assistant-frontend==20180326.0 +home-assistant-frontend==20180330.0 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From d897a07d0be7c1fd62a838344fef4f1745b72a23 Mon Sep 17 00:00:00 2001 From: Philip Rosenberg-Watt Date: Mon, 26 Mar 2018 19:10:22 -0600 Subject: [PATCH 206/220] Fix Google Calendar caching when offline (#13375) * Fix Google Calendar caching when offline Events from Google Calendar were not firing under the following circumstances: 1. Start ha as normal with Google Calendar configured as per instructions. 2. ha loses network connectivity to Google 3. ha attempts update of Google Calendar 4. calendar/google component throws uncaught Exception causing update method to not return 5. (cached) Google Calendar event does not fire, remains "Off" Catching the Exception and returning False from the update() method causes the correct behavior (i.e., the calendar component firing the event as scheduled using cached data). * Add requirements * Revert code cleanup * Remove explicit return value from update() * Revert "Remove explicit return value from update()" This reverts commit 7cd77708af658ccea855de47a32ce4ac5262ac30. * Use MockDependency decorator No need to whitelist google-python-api-client for a single unit test at this point. --- homeassistant/components/calendar/google.py | 9 ++++++++- tests/components/calendar/test_google.py | 17 +++++++++++++++-- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/calendar/google.py b/homeassistant/components/calendar/google.py index 098c7c70834..a8763e8ca9e 100644 --- a/homeassistant/components/calendar/google.py +++ b/homeassistant/components/calendar/google.py @@ -62,7 +62,14 @@ class GoogleCalendarData(object): @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Get the latest data.""" - service = self.calendar_service.get() + from httplib2 import ServerNotFoundError + + try: + service = self.calendar_service.get() + except ServerNotFoundError: + _LOGGER.warning("Unable to connect to Google, using cached data") + return False + params = dict(DEFAULT_GOOGLE_SEARCH_PARAMS) params['timeMin'] = dt.now().isoformat('T') params['calendarId'] = self.calendar_id diff --git a/tests/components/calendar/test_google.py b/tests/components/calendar/test_google.py index 62c8ea8854f..9f94ea9f44c 100644 --- a/tests/components/calendar/test_google.py +++ b/tests/components/calendar/test_google.py @@ -2,7 +2,7 @@ # pylint: disable=protected-access import logging import unittest -from unittest.mock import patch +from unittest.mock import patch, Mock import pytest @@ -11,7 +11,7 @@ import homeassistant.components.calendar.google as calendar import homeassistant.util.dt as dt_util from homeassistant.const import CONF_PLATFORM, STATE_OFF, STATE_ON from homeassistant.helpers.template import DATE_STR_FORMAT -from tests.common import get_test_home_assistant +from tests.common import get_test_home_assistant, MockDependency TEST_PLATFORM = {calendar_base.DOMAIN: {CONF_PLATFORM: 'test'}} @@ -421,3 +421,16 @@ class TestComponentsGoogleCalendar(unittest.TestCase): 'location': event['location'], 'description': event['description'] }) + + @MockDependency("httplib2") + def test_update_false(self, mock_httplib2): + """Test that the update returns False upon Error.""" + mock_service = Mock() + mock_service.get = Mock( + side_effect=mock_httplib2.ServerNotFoundError("unit test")) + + cal = calendar.GoogleCalendarEventDevice(self.hass, mock_service, None, + {'name': "test"}) + result = cal.data.update() + + self.assertFalse(result) From 020669fc6026a4083062debef33ccb4d400ae71c Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Tue, 27 Mar 2018 11:31:18 +0200 Subject: [PATCH 207/220] Homekit: Bugfix Thermostat Fahrenheit support (#13477) * Bugfix thermostat temperature conversion * util -> temperature_to_homekit * util -> temperature_to_states * util -> convert_to_float * Added tests, deleted log msg --- homeassistant/components/homekit/__init__.py | 3 -- .../components/homekit/type_sensors.py | 35 ++++------------ .../components/homekit/type_thermostats.py | 33 ++++++++++----- homeassistant/components/homekit/util.py | 21 +++++++++- .../homekit/test_get_accessories.py | 14 +++++++ tests/components/homekit/test_type_sensors.py | 21 +--------- .../homekit/test_type_thermostats.py | 41 ++++++++++++++++++- tests/components/homekit/test_util.py | 23 ++++++++++- 8 files changed, 126 insertions(+), 65 deletions(-) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 4854a828e41..8ef8445aa70 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -186,9 +186,6 @@ class HomeKit(): for state in self._hass.states.all(): self.add_bridge_accessory(state) - for entity_id in self._config: - _LOGGER.warning('The entity "%s" was not setup when HomeKit ' - 'was started', entity_id) self.bridge.set_broker(self.driver) if not self.bridge.paired: diff --git a/homeassistant/components/homekit/type_sensors.py b/homeassistant/components/homekit/type_sensors.py index 7575acb5c35..e980ce4a316 100644 --- a/homeassistant/components/homekit/type_sensors.py +++ b/homeassistant/components/homekit/type_sensors.py @@ -2,8 +2,7 @@ import logging from homeassistant.const import ( - ATTR_UNIT_OF_MEASUREMENT, TEMP_FAHRENHEIT, TEMP_CELSIUS) -from homeassistant.util.temperature import fahrenheit_to_celsius + ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS) from . import TYPES from .accessories import ( @@ -11,33 +10,12 @@ from .accessories import ( from .const import ( CATEGORY_SENSOR, SERV_HUMIDITY_SENSOR, SERV_TEMPERATURE_SENSOR, CHAR_CURRENT_HUMIDITY, CHAR_CURRENT_TEMPERATURE, PROP_CELSIUS) +from .util import convert_to_float, temperature_to_homekit _LOGGER = logging.getLogger(__name__) -def calc_temperature(state, unit=TEMP_CELSIUS): - """Calculate temperature from state and unit. - - Always return temperature as Celsius value. - Conversion is handled on the device. - """ - try: - value = float(state) - except ValueError: - return None - - return fahrenheit_to_celsius(value) if unit == TEMP_FAHRENHEIT else value - - -def calc_humidity(state): - """Calculate humidity from state.""" - try: - return float(state) - except ValueError: - return None - - @TYPES.register('TemperatureSensor') class TemperatureSensor(HomeAccessory): """Generate a TemperatureSensor accessory for a temperature sensor. @@ -63,9 +41,10 @@ class TemperatureSensor(HomeAccessory): if new_state is None: return - unit = new_state.attributes[ATTR_UNIT_OF_MEASUREMENT] - temperature = calc_temperature(new_state.state, unit) + unit = new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS) + temperature = convert_to_float(new_state.state) if temperature: + temperature = temperature_to_homekit(temperature, unit) self.char_temp.set_value(temperature, should_callback=False) _LOGGER.debug('%s: Current temperature set to %d°C', self._entity_id, temperature) @@ -92,8 +71,8 @@ class HumiditySensor(HomeAccessory): if new_state is None: return - humidity = calc_humidity(new_state.state) + humidity = convert_to_float(new_state.state) if humidity: self.char_humidity.set_value(humidity, should_callback=False) - _LOGGER.debug('%s: Current humidity set to %d%%', + _LOGGER.debug('%s: Percent set to %d%%', self._entity_id, humidity) diff --git a/homeassistant/components/homekit/type_thermostats.py b/homeassistant/components/homekit/type_thermostats.py index 3f545e90eb3..d49c1ca626b 100644 --- a/homeassistant/components/homekit/type_thermostats.py +++ b/homeassistant/components/homekit/type_thermostats.py @@ -16,6 +16,7 @@ from .const import ( CHAR_TARGET_HEATING_COOLING, CHAR_CURRENT_TEMPERATURE, CHAR_TARGET_TEMPERATURE, CHAR_TEMP_DISPLAY_UNITS, CHAR_COOLING_THRESHOLD_TEMPERATURE, CHAR_HEATING_THRESHOLD_TEMPERATURE) +from .util import temperature_to_homekit, temperature_to_states _LOGGER = logging.getLogger(__name__) @@ -40,6 +41,7 @@ class Thermostat(HomeAccessory): self._hass = hass self._entity_id = entity_id self._call_timer = None + self._unit = TEMP_CELSIUS self.heat_cool_flag_target_state = False self.temperature_flag_target_state = False @@ -107,33 +109,38 @@ class Thermostat(HomeAccessory): def set_cooling_threshold(self, value): """Set cooling threshold temp to value if call came from HomeKit.""" - _LOGGER.debug('%s: Set cooling threshold temperature to %.2f', + _LOGGER.debug('%s: Set cooling threshold temperature to %.2f°C', self._entity_id, value) self.coolingthresh_flag_target_state = True self.char_cooling_thresh_temp.set_value(value, should_callback=False) low = self.char_heating_thresh_temp.value + low = temperature_to_states(low, self._unit) + value = temperature_to_states(value, self._unit) self._hass.components.climate.set_temperature( entity_id=self._entity_id, target_temp_high=value, target_temp_low=low) def set_heating_threshold(self, value): """Set heating threshold temp to value if call came from HomeKit.""" - _LOGGER.debug('%s: Set heating threshold temperature to %.2f', + _LOGGER.debug('%s: Set heating threshold temperature to %.2f°C', self._entity_id, value) self.heatingthresh_flag_target_state = True self.char_heating_thresh_temp.set_value(value, should_callback=False) # Home assistant always wants to set low and high at the same time high = self.char_cooling_thresh_temp.value + high = temperature_to_states(high, self._unit) + value = temperature_to_states(value, self._unit) self._hass.components.climate.set_temperature( entity_id=self._entity_id, target_temp_high=high, target_temp_low=value) def set_target_temperature(self, value): """Set target temperature to value if call came from HomeKit.""" - _LOGGER.debug('%s: Set target temperature to %.2f', + _LOGGER.debug('%s: Set target temperature to %.2f°C', self._entity_id, value) self.temperature_flag_target_state = True self.char_target_temp.set_value(value, should_callback=False) + value = temperature_to_states(value, self._unit) self._hass.components.climate.set_temperature( temperature=value, entity_id=self._entity_id) @@ -142,14 +149,19 @@ class Thermostat(HomeAccessory): if new_state is None: return + self._unit = new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT, + TEMP_CELSIUS) + # Update current temperature current_temp = new_state.attributes.get(ATTR_CURRENT_TEMPERATURE) if isinstance(current_temp, (int, float)): + current_temp = temperature_to_homekit(current_temp, self._unit) self.char_current_temp.set_value(current_temp) # Update target temperature target_temp = new_state.attributes.get(ATTR_TEMPERATURE) if isinstance(target_temp, (int, float)): + target_temp = temperature_to_homekit(target_temp, self._unit) if not self.temperature_flag_target_state: self.char_target_temp.set_value(target_temp, should_callback=False) @@ -158,7 +170,9 @@ class Thermostat(HomeAccessory): # Update cooling threshold temperature if characteristic exists if self.char_cooling_thresh_temp: cooling_thresh = new_state.attributes.get(ATTR_TARGET_TEMP_HIGH) - if cooling_thresh: + if isinstance(cooling_thresh, (int, float)): + cooling_thresh = temperature_to_homekit(cooling_thresh, + self._unit) if not self.coolingthresh_flag_target_state: self.char_cooling_thresh_temp.set_value( cooling_thresh, should_callback=False) @@ -167,18 +181,17 @@ class Thermostat(HomeAccessory): # Update heating threshold temperature if characteristic exists if self.char_heating_thresh_temp: heating_thresh = new_state.attributes.get(ATTR_TARGET_TEMP_LOW) - if heating_thresh: + if isinstance(heating_thresh, (int, float)): + heating_thresh = temperature_to_homekit(heating_thresh, + self._unit) if not self.heatingthresh_flag_target_state: self.char_heating_thresh_temp.set_value( heating_thresh, should_callback=False) self.heatingthresh_flag_target_state = False # Update display units - display_units = new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - if display_units \ - and display_units in UNIT_HASS_TO_HOMEKIT: - self.char_display_units.set_value( - UNIT_HASS_TO_HOMEKIT[display_units]) + if self._unit and self._unit in UNIT_HASS_TO_HOMEKIT: + self.char_display_units.set_value(UNIT_HASS_TO_HOMEKIT[self._unit]) # Update target operation mode operation_mode = new_state.attributes.get(ATTR_OPERATION_MODE) diff --git a/homeassistant/components/homekit/util.py b/homeassistant/components/homekit/util.py index f18eb2273db..2fa2ebd396a 100644 --- a/homeassistant/components/homekit/util.py +++ b/homeassistant/components/homekit/util.py @@ -5,8 +5,9 @@ import voluptuous as vol from homeassistant.core import split_entity_id from homeassistant.const import ( - ATTR_CODE) + ATTR_CODE, TEMP_CELSIUS) import homeassistant.helpers.config_validation as cv +import homeassistant.util.temperature as temp_util from .const import HOMEKIT_NOTIFY_ID _LOGGER = logging.getLogger(__name__) @@ -44,3 +45,21 @@ def show_setup_message(bridge, hass): def dismiss_setup_message(hass): """Dismiss persistent notification and remove QR code.""" hass.components.persistent_notification.dismiss(HOMEKIT_NOTIFY_ID) + + +def convert_to_float(state): + """Return float of state, catch errors.""" + try: + return float(state) + except (ValueError, TypeError): + return None + + +def temperature_to_homekit(temperature, unit): + """Convert temperature to Celsius for HomeKit.""" + return round(temp_util.convert(temperature, unit, TEMP_CELSIUS), 1) + + +def temperature_to_states(temperature, unit): + """Convert temperature back from Celsius to Home Assistant unit.""" + return round(temp_util.convert(temperature, TEMP_CELSIUS, unit), 1) diff --git a/tests/components/homekit/test_get_accessories.py b/tests/components/homekit/test_get_accessories.py index ee7baae2755..e29ed85b5fc 100644 --- a/tests/components/homekit/test_get_accessories.py +++ b/tests/components/homekit/test_get_accessories.py @@ -16,6 +16,20 @@ _LOGGER = logging.getLogger(__name__) CONFIG = {} +def test_get_accessory_invalid_aid(caplog): + """Test with unsupported component.""" + assert get_accessory(None, State('light.demo', 'on'), + aid=None, config=None) is None + assert caplog.records[0].levelname == 'WARNING' + assert 'invalid aid' in caplog.records[0].msg + + +def test_not_supported(): + """Test if none is returned if entity isn't supported.""" + assert get_accessory(None, State('demo.demo', 'on'), aid=2, config=None) \ + is None + + class TestGetAccessories(unittest.TestCase): """Methods to test the get_accessory method.""" diff --git a/tests/components/homekit/test_type_sensors.py b/tests/components/homekit/test_type_sensors.py index 551dfc6780d..c04c250613d 100644 --- a/tests/components/homekit/test_type_sensors.py +++ b/tests/components/homekit/test_type_sensors.py @@ -3,32 +3,13 @@ import unittest from homeassistant.components.homekit.const import PROP_CELSIUS from homeassistant.components.homekit.type_sensors import ( - TemperatureSensor, HumiditySensor, calc_temperature, calc_humidity) + TemperatureSensor, HumiditySensor) from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, STATE_UNKNOWN, TEMP_CELSIUS, TEMP_FAHRENHEIT) from tests.common import get_test_home_assistant -def test_calc_temperature(): - """Test if temperature in Celsius is calculated correctly.""" - assert calc_temperature(STATE_UNKNOWN) is None - assert calc_temperature('test') is None - - assert calc_temperature('20') == 20 - assert calc_temperature('20.12', TEMP_CELSIUS) == 20.12 - assert calc_temperature('75.2', TEMP_FAHRENHEIT) == 24 - - -def test_calc_humidity(): - """Test if humidity is a integer.""" - assert calc_humidity(STATE_UNKNOWN) is None - assert calc_humidity('test') is None - - assert calc_humidity('20') == 20 - assert calc_humidity('75.2') == 75.2 - - class TestHomekitSensors(unittest.TestCase): """Test class for all accessory types regarding sensors.""" diff --git a/tests/components/homekit/test_type_thermostats.py b/tests/components/homekit/test_type_thermostats.py index 6505bf72efb..011fe73377d 100644 --- a/tests/components/homekit/test_type_thermostats.py +++ b/tests/components/homekit/test_type_thermostats.py @@ -10,7 +10,7 @@ from homeassistant.components.homekit.type_thermostats import ( Thermostat, STATE_OFF) from homeassistant.const import ( ATTR_SERVICE, EVENT_CALL_SERVICE, ATTR_SERVICE_DATA, - ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS) + ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS, TEMP_FAHRENHEIT) from tests.common import get_test_home_assistant @@ -238,3 +238,42 @@ class TestHomekitThermostats(unittest.TestCase): self.events[1].data[ATTR_SERVICE_DATA][ATTR_TARGET_TEMP_HIGH], 25.0) self.assertEqual(acc.char_cooling_thresh_temp.value, 25.0) + + def test_thermostat_fahrenheit(self): + """Test if accessory and HA are updated accordingly.""" + climate = 'climate.test' + + acc = Thermostat(self.hass, climate, 'Climate', True) + acc.run() + + self.hass.states.set(climate, STATE_AUTO, + {ATTR_OPERATION_MODE: STATE_AUTO, + ATTR_TARGET_TEMP_HIGH: 75.2, + ATTR_TARGET_TEMP_LOW: 68, + ATTR_TEMPERATURE: 71.6, + ATTR_CURRENT_TEMPERATURE: 73.4, + ATTR_UNIT_OF_MEASUREMENT: TEMP_FAHRENHEIT}) + self.hass.block_till_done() + self.assertEqual(acc.char_heating_thresh_temp.value, 20.0) + self.assertEqual(acc.char_cooling_thresh_temp.value, 24.0) + self.assertEqual(acc.char_current_temp.value, 23.0) + self.assertEqual(acc.char_target_temp.value, 22.0) + self.assertEqual(acc.char_display_units.value, 1) + + # Set from HomeKit + acc.char_cooling_thresh_temp.set_value(23) + self.hass.block_till_done() + service_data = self.events[-1].data[ATTR_SERVICE_DATA] + self.assertEqual(service_data[ATTR_TARGET_TEMP_HIGH], 73.4) + self.assertEqual(service_data[ATTR_TARGET_TEMP_LOW], 68) + + acc.char_heating_thresh_temp.set_value(22) + self.hass.block_till_done() + service_data = self.events[-1].data[ATTR_SERVICE_DATA] + self.assertEqual(service_data[ATTR_TARGET_TEMP_HIGH], 73.4) + self.assertEqual(service_data[ATTR_TARGET_TEMP_LOW], 71.6) + + acc.char_target_temp.set_value(24.0) + self.hass.block_till_done() + service_data = self.events[-1].data[ATTR_SERVICE_DATA] + self.assertEqual(service_data[ATTR_TEMPERATURE], 75.2) diff --git a/tests/components/homekit/test_util.py b/tests/components/homekit/test_util.py index f95db9a4a13..d6ef5856f85 100644 --- a/tests/components/homekit/test_util.py +++ b/tests/components/homekit/test_util.py @@ -7,13 +7,15 @@ from homeassistant.core import callback from homeassistant.components.homekit.accessories import HomeBridge from homeassistant.components.homekit.const import HOMEKIT_NOTIFY_ID from homeassistant.components.homekit.util import ( - show_setup_message, dismiss_setup_message, ATTR_CODE) + show_setup_message, dismiss_setup_message, convert_to_float, + temperature_to_homekit, temperature_to_states, ATTR_CODE) from homeassistant.components.homekit.util import validate_entity_config \ as vec from homeassistant.components.persistent_notification import ( SERVICE_CREATE, SERVICE_DISMISS, ATTR_NOTIFICATION_ID) from homeassistant.const import ( - EVENT_CALL_SERVICE, ATTR_DOMAIN, ATTR_SERVICE, ATTR_SERVICE_DATA) + EVENT_CALL_SERVICE, ATTR_DOMAIN, ATTR_SERVICE, ATTR_SERVICE_DATA, + TEMP_CELSIUS, TEMP_FAHRENHEIT, STATE_UNKNOWN) from tests.common import get_test_home_assistant @@ -81,3 +83,20 @@ class TestUtil(unittest.TestCase): self.assertEqual( data[ATTR_SERVICE_DATA].get(ATTR_NOTIFICATION_ID, None), HOMEKIT_NOTIFY_ID) + + def test_convert_to_float(self): + """Test convert_to_float method.""" + self.assertEqual(convert_to_float(12), 12) + self.assertEqual(convert_to_float(12.4), 12.4) + self.assertIsNone(convert_to_float(STATE_UNKNOWN)) + self.assertIsNone(convert_to_float(None)) + + def test_temperature_to_homekit(self): + """Test temperature conversion from HA to HomeKit.""" + self.assertEqual(temperature_to_homekit(20.46, TEMP_CELSIUS), 20.5) + self.assertEqual(temperature_to_homekit(92.1, TEMP_FAHRENHEIT), 33.4) + + def test_temperature_to_states(self): + """Test temperature conversion from HomeKit to HA.""" + self.assertEqual(temperature_to_states(20, TEMP_CELSIUS), 20.0) + self.assertEqual(temperature_to_states(20.2, TEMP_FAHRENHEIT), 68.4) From e04b01daad3b25335fc9479e1552e44915c9fc7f Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Tue, 27 Mar 2018 04:50:29 +0200 Subject: [PATCH 208/220] Validate basic customize entries (#13478) * Added schema to validate customize dictionary * Added test --- homeassistant/config.py | 13 ++++++++++--- tests/test_config.py | 24 ++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/homeassistant/config.py b/homeassistant/config.py index 58cfe845e8f..53e611ac725 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -13,6 +13,7 @@ import voluptuous as vol from voluptuous.humanize import humanize_error from homeassistant.const import ( + ATTR_FRIENDLY_NAME, ATTR_HIDDEN, ATTR_ASSUMED_STATE, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, CONF_PACKAGES, CONF_UNIT_SYSTEM, CONF_TIME_ZONE, CONF_ELEVATION, CONF_UNIT_SYSTEM_METRIC, CONF_UNIT_SYSTEM_IMPERIAL, CONF_TEMPERATURE_UNIT, TEMP_CELSIUS, @@ -129,13 +130,19 @@ PACKAGES_CONFIG_SCHEMA = vol.Schema({ {cv.slug: vol.Any(dict, list, None)}) # Only slugs for component names }) +CUSTOMIZE_DICT_SCHEMA = vol.Schema({ + vol.Optional(ATTR_FRIENDLY_NAME): cv.string, + vol.Optional(ATTR_HIDDEN): cv.boolean, + vol.Optional(ATTR_ASSUMED_STATE): cv.boolean, +}, extra=vol.ALLOW_EXTRA) + CUSTOMIZE_CONFIG_SCHEMA = vol.Schema({ vol.Optional(CONF_CUSTOMIZE, default={}): - vol.Schema({cv.entity_id: dict}), + vol.Schema({cv.entity_id: CUSTOMIZE_DICT_SCHEMA}), vol.Optional(CONF_CUSTOMIZE_DOMAIN, default={}): - vol.Schema({cv.string: dict}), + vol.Schema({cv.string: CUSTOMIZE_DICT_SCHEMA}), vol.Optional(CONF_CUSTOMIZE_GLOB, default={}): - vol.Schema({cv.string: OrderedDict}), + vol.Schema({cv.string: CUSTOMIZE_DICT_SCHEMA}), }) CORE_CONFIG_SCHEMA = CUSTOMIZE_CONFIG_SCHEMA.extend({ diff --git a/tests/test_config.py b/tests/test_config.py index aaa793f91a9..22fcebc6ea4 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -12,6 +12,7 @@ from voluptuous import MultipleInvalid from homeassistant.core import DOMAIN, HomeAssistantError, Config import homeassistant.config as config_util from homeassistant.const import ( + ATTR_FRIENDLY_NAME, ATTR_HIDDEN, ATTR_ASSUMED_STATE, CONF_LATITUDE, CONF_LONGITUDE, CONF_UNIT_SYSTEM, CONF_NAME, CONF_TIME_ZONE, CONF_ELEVATION, CONF_CUSTOMIZE, __version__, CONF_UNIT_SYSTEM_METRIC, CONF_UNIT_SYSTEM_IMPERIAL, CONF_TEMPERATURE_UNIT) @@ -235,6 +236,29 @@ class TestConfig(unittest.TestCase): }, }) + def test_customize_dict_schema(self): + """Test basic customize config validation.""" + values = ( + {ATTR_FRIENDLY_NAME: None}, + {ATTR_HIDDEN: '2'}, + {ATTR_ASSUMED_STATE: '2'}, + ) + + for val in values: + print(val) + with pytest.raises(MultipleInvalid): + config_util.CUSTOMIZE_DICT_SCHEMA(val) + + assert config_util.CUSTOMIZE_DICT_SCHEMA({ + ATTR_FRIENDLY_NAME: 2, + ATTR_HIDDEN: '1', + ATTR_ASSUMED_STATE: '0', + }) == { + ATTR_FRIENDLY_NAME: '2', + ATTR_HIDDEN: True, + ATTR_ASSUMED_STATE: False + } + def test_customize_glob_is_ordered(self): """Test that customize_glob preserves order.""" conf = config_util.CORE_CONFIG_SCHEMA( From b0073b437f04510a305286d54e1eeb989bee8518 Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Tue, 27 Mar 2018 23:39:25 +0200 Subject: [PATCH 209/220] Homekit: Fix security systems (#13499) * Fix alarm_code=None * Added test --- .../components/homekit/type_security_systems.py | 4 +++- .../homekit/test_type_security_systems.py | 16 ++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/homekit/type_security_systems.py b/homeassistant/components/homekit/type_security_systems.py index 146fca95b53..b23522f0ea2 100644 --- a/homeassistant/components/homekit/type_security_systems.py +++ b/homeassistant/components/homekit/type_security_systems.py @@ -58,7 +58,9 @@ class SecuritySystem(HomeAccessory): hass_value = HOMEKIT_TO_HASS[value] service = STATE_TO_SERVICE[hass_value] - params = {ATTR_ENTITY_ID: self._entity_id, ATTR_CODE: self._alarm_code} + params = {ATTR_ENTITY_ID: self._entity_id} + if self._alarm_code: + params[ATTR_CODE] = self._alarm_code self._hass.services.call('alarm_control_panel', service, params) def update_state(self, entity_id=None, old_state=None, new_state=None): diff --git a/tests/components/homekit/test_type_security_systems.py b/tests/components/homekit/test_type_security_systems.py index 4d61fc4a44c..c689a73bac2 100644 --- a/tests/components/homekit/test_type_security_systems.py +++ b/tests/components/homekit/test_type_security_systems.py @@ -102,3 +102,19 @@ class TestHomekitSecuritySystems(unittest.TestCase): self.assertEqual( self.events[0].data[ATTR_SERVICE_DATA][ATTR_CODE], '1234') self.assertEqual(acc.char_target_state.value, 3) + + def test_no_alarm_code(self): + """Test accessory if security_system doesn't require a alarm_code.""" + acp = 'alarm_control_panel.test' + + acc = SecuritySystem(self.hass, acp, 'SecuritySystem', + alarm_code=None, aid=2) + acc.run() + + # Set from HomeKit + acc.char_target_state.set_value(0) + self.hass.block_till_done() + self.assertEqual( + self.events[0].data[ATTR_SERVICE], 'alarm_arm_home') + self.assertNotIn(ATTR_CODE, self.events[0].data[ATTR_SERVICE_DATA]) + self.assertEqual(acc.char_target_state.value, 0) From 26fb3d7faa2c4eb485b5450fb5838c41128d66dd Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Thu, 29 Mar 2018 00:55:05 +0200 Subject: [PATCH 210/220] python-miio version bumped (Closes: 13449) (#13511) --- homeassistant/components/fan/xiaomi_miio.py | 2 +- homeassistant/components/light/xiaomi_miio.py | 2 +- homeassistant/components/remote/xiaomi_miio.py | 2 +- homeassistant/components/sensor/xiaomi_miio.py | 2 +- homeassistant/components/switch/xiaomi_miio.py | 2 +- homeassistant/components/vacuum/xiaomi_miio.py | 2 +- requirements_all.txt | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/fan/xiaomi_miio.py b/homeassistant/components/fan/xiaomi_miio.py index 09df55200a2..3e6aee6ba3a 100644 --- a/homeassistant/components/fan/xiaomi_miio.py +++ b/homeassistant/components/fan/xiaomi_miio.py @@ -29,7 +29,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, }) -REQUIREMENTS = ['python-miio==0.3.8'] +REQUIREMENTS = ['python-miio==0.3.9'] ATTR_TEMPERATURE = 'temperature' ATTR_HUMIDITY = 'humidity' diff --git a/homeassistant/components/light/xiaomi_miio.py b/homeassistant/components/light/xiaomi_miio.py index a21c86f49c0..999d0f7f0e6 100644 --- a/homeassistant/components/light/xiaomi_miio.py +++ b/homeassistant/components/light/xiaomi_miio.py @@ -41,7 +41,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ 'philips.light.candle2']), }) -REQUIREMENTS = ['python-miio==0.3.8'] +REQUIREMENTS = ['python-miio==0.3.9'] # The light does not accept cct values < 1 CCT_MIN = 1 diff --git a/homeassistant/components/remote/xiaomi_miio.py b/homeassistant/components/remote/xiaomi_miio.py index 91f753391fc..13ec9c873b1 100644 --- a/homeassistant/components/remote/xiaomi_miio.py +++ b/homeassistant/components/remote/xiaomi_miio.py @@ -22,7 +22,7 @@ from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv from homeassistant.util.dt import utcnow -REQUIREMENTS = ['python-miio==0.3.8'] +REQUIREMENTS = ['python-miio==0.3.9'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sensor/xiaomi_miio.py b/homeassistant/components/sensor/xiaomi_miio.py index cb172735ac4..33ba5793fe0 100644 --- a/homeassistant/components/sensor/xiaomi_miio.py +++ b/homeassistant/components/sensor/xiaomi_miio.py @@ -25,7 +25,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, }) -REQUIREMENTS = ['python-miio==0.3.8'] +REQUIREMENTS = ['python-miio==0.3.9'] ATTR_POWER = 'power' ATTR_CHARGING = 'charging' diff --git a/homeassistant/components/switch/xiaomi_miio.py b/homeassistant/components/switch/xiaomi_miio.py index 9f0f163df69..27c3c4c72f1 100644 --- a/homeassistant/components/switch/xiaomi_miio.py +++ b/homeassistant/components/switch/xiaomi_miio.py @@ -37,7 +37,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ 'chuangmi.plug.v2']), }) -REQUIREMENTS = ['python-miio==0.3.8'] +REQUIREMENTS = ['python-miio==0.3.9'] ATTR_POWER = 'power' ATTR_TEMPERATURE = 'temperature' diff --git a/homeassistant/components/vacuum/xiaomi_miio.py b/homeassistant/components/vacuum/xiaomi_miio.py index f42a895f94f..887a50fdcce 100644 --- a/homeassistant/components/vacuum/xiaomi_miio.py +++ b/homeassistant/components/vacuum/xiaomi_miio.py @@ -19,7 +19,7 @@ from homeassistant.const import ( ATTR_ENTITY_ID, CONF_HOST, CONF_NAME, CONF_TOKEN, STATE_OFF, STATE_ON) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['python-miio==0.3.8'] +REQUIREMENTS = ['python-miio==0.3.9'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index df81dae9f00..b900328cab5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -948,7 +948,7 @@ python-juicenet==0.0.5 # homeassistant.components.sensor.xiaomi_miio # homeassistant.components.switch.xiaomi_miio # homeassistant.components.vacuum.xiaomi_miio -python-miio==0.3.8 +python-miio==0.3.9 # homeassistant.components.media_player.mpd python-mpd2==0.5.5 From e993d095cbff9976f45141944054d39b44ceaf1c Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Fri, 30 Mar 2018 02:10:56 +0200 Subject: [PATCH 211/220] Fix mysensors light supported features (#13512) * Different types of light should have different supported features. --- homeassistant/components/light/mysensors.py | 29 ++++++++++++++++----- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/light/mysensors.py b/homeassistant/components/light/mysensors.py index 14a770b7632..7aa1e754c43 100644 --- a/homeassistant/components/light/mysensors.py +++ b/homeassistant/components/light/mysensors.py @@ -12,8 +12,7 @@ from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.util.color import rgb_hex_to_rgb_list import homeassistant.util.color as color_util -SUPPORT_MYSENSORS = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR | - SUPPORT_WHITE_VALUE) +SUPPORT_MYSENSORS_RGBW = SUPPORT_COLOR | SUPPORT_WHITE_VALUE def setup_platform(hass, config, add_devices, discovery_info=None): @@ -64,11 +63,6 @@ class MySensorsLight(mysensors.MySensorsEntity, Light): """Return true if device is on.""" return self._state - @property - def supported_features(self): - """Flag supported features.""" - return SUPPORT_MYSENSORS - def _turn_on_light(self): """Turn on light child device.""" set_req = self.gateway.const.SetReq @@ -171,6 +165,11 @@ class MySensorsLight(mysensors.MySensorsEntity, Light): class MySensorsLightDimmer(MySensorsLight): """Dimmer child class to MySensorsLight.""" + @property + def supported_features(self): + """Flag supported features.""" + return SUPPORT_BRIGHTNESS + def turn_on(self, **kwargs): """Turn the device on.""" self._turn_on_light() @@ -188,6 +187,14 @@ class MySensorsLightDimmer(MySensorsLight): class MySensorsLightRGB(MySensorsLight): """RGB child class to MySensorsLight.""" + @property + def supported_features(self): + """Flag supported features.""" + set_req = self.gateway.const.SetReq + if set_req.V_DIMMER in self._values: + return SUPPORT_BRIGHTNESS | SUPPORT_COLOR + return SUPPORT_COLOR + def turn_on(self, **kwargs): """Turn the device on.""" self._turn_on_light() @@ -209,6 +216,14 @@ class MySensorsLightRGBW(MySensorsLightRGB): # pylint: disable=too-many-ancestors + @property + def supported_features(self): + """Flag supported features.""" + set_req = self.gateway.const.SetReq + if set_req.V_DIMMER in self._values: + return SUPPORT_BRIGHTNESS | SUPPORT_MYSENSORS_RGBW + return SUPPORT_MYSENSORS_RGBW + def turn_on(self, **kwargs): """Turn the device on.""" self._turn_on_light() From dfd15900c76de6c286a6a7555cd19ddc5f6f32eb Mon Sep 17 00:00:00 2001 From: Tom Harris Date: Thu, 29 Mar 2018 20:10:27 -0400 Subject: [PATCH 212/220] Fix Insteon Leak Sensor (#13515) * update leak sensor * Fix error when insteon state type is unknown * Bump insteon version to 0.8.3 * Update requirements all and test * Fix requirements conflicts due to lack of commit sync * Requirements sync * Rerun script/gen_requirements_all.py * Try requirements update again * Update requirements --- .../components/binary_sensor/insteon_plm.py | 16 +++++----- homeassistant/components/insteon_plm.py | 31 ++++++++++--------- requirements_all.txt | 2 +- 3 files changed, 26 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/binary_sensor/insteon_plm.py b/homeassistant/components/binary_sensor/insteon_plm.py index 09c4b5c8ea7..06079d6aa3b 100644 --- a/homeassistant/components/binary_sensor/insteon_plm.py +++ b/homeassistant/components/binary_sensor/insteon_plm.py @@ -17,7 +17,7 @@ _LOGGER = logging.getLogger(__name__) SENSOR_TYPES = {'openClosedSensor': 'opening', 'motionSensor': 'motion', 'doorSensor': 'door', - 'leakSensor': 'moisture'} + 'wetLeakSensor': 'moisture'} @asyncio.coroutine @@ -28,13 +28,14 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): address = discovery_info['address'] device = plm.devices[address] state_key = discovery_info['state_key'] + name = device.states[state_key].name + if name != 'dryLeakSensor': + _LOGGER.debug('Adding device %s entity %s to Binary Sensor platform', + device.address.hex, device.states[state_key].name) - _LOGGER.debug('Adding device %s entity %s to Binary Sensor platform', - device.address.hex, device.states[state_key].name) + new_entity = InsteonPLMBinarySensor(device, state_key) - new_entity = InsteonPLMBinarySensor(device, state_key) - - async_add_devices([new_entity]) + async_add_devices([new_entity]) class InsteonPLMBinarySensor(InsteonPLMEntity, BinarySensorDevice): @@ -53,5 +54,4 @@ class InsteonPLMBinarySensor(InsteonPLMEntity, BinarySensorDevice): @property def is_on(self): """Return the boolean response if the node is on.""" - sensorstate = self._insteon_device_state.value - return bool(sensorstate) + return bool(self._insteon_device_state.value) diff --git a/homeassistant/components/insteon_plm.py b/homeassistant/components/insteon_plm.py index 2381e3db69e..6f5c5223ea0 100644 --- a/homeassistant/components/insteon_plm.py +++ b/homeassistant/components/insteon_plm.py @@ -16,7 +16,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers import discovery from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['insteonplm==0.8.2'] +REQUIREMENTS = ['insteonplm==0.8.3'] _LOGGER = logging.getLogger(__name__) @@ -64,19 +64,20 @@ def async_setup(hass, config): """Detect device from transport to be delegated to platform.""" for state_key in device.states: platform_info = ipdb[device.states[state_key]] - platform = platform_info.platform - if platform is not None: - _LOGGER.info("New INSTEON PLM device: %s (%s) %s", - device.address, - device.states[state_key].name, - platform) + if platform_info: + platform = platform_info.platform + if platform: + _LOGGER.info("New INSTEON PLM device: %s (%s) %s", + device.address, + device.states[state_key].name, + platform) - hass.async_add_job( - discovery.async_load_platform( - hass, platform, DOMAIN, - discovered={'address': device.address.hex, - 'state_key': state_key}, - hass_config=config)) + hass.async_add_job( + discovery.async_load_platform( + hass, platform, DOMAIN, + discovered={'address': device.address.hex, + 'state_key': state_key}, + hass_config=config)) _LOGGER.info("Looking for PLM on %s", port) conn = yield from insteonplm.Connection.create( @@ -127,13 +128,15 @@ class IPDB(object): from insteonplm.states.sensor import (VariableSensor, OnOffSensor, SmokeCO2Sensor, - IoLincSensor) + IoLincSensor, + LeakSensorDryWet) self.states = [State(OnOffSwitch_OutletTop, 'switch'), State(OnOffSwitch_OutletBottom, 'switch'), State(OpenClosedRelay, 'switch'), State(OnOffSwitch, 'switch'), + State(LeakSensorDryWet, 'binary_sensor'), State(IoLincSensor, 'binary_sensor'), State(SmokeCO2Sensor, 'sensor'), State(OnOffSensor, 'binary_sensor'), diff --git a/requirements_all.txt b/requirements_all.txt index b900328cab5..a72d33e35ec 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -417,7 +417,7 @@ influxdb==5.0.0 insteonlocal==0.53 # homeassistant.components.insteon_plm -insteonplm==0.8.2 +insteonplm==0.8.3 # homeassistant.components.verisure jsonpath==0.75 From 0428559f694e4ec67061ccc087d13e2bc688c786 Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Thu, 29 Mar 2018 18:35:57 +0200 Subject: [PATCH 213/220] HomeKit: Fix setting light brightness (#13518) * Added test --- .../components/homekit/type_lights.py | 21 +++++--- tests/components/homekit/test_type_lights.py | 50 ++++++++++++------- 2 files changed, 44 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/homekit/type_lights.py b/homeassistant/components/homekit/type_lights.py index 2415bb1a4df..d88e7100131 100644 --- a/homeassistant/components/homekit/type_lights.py +++ b/homeassistant/components/homekit/type_lights.py @@ -32,6 +32,7 @@ class Light(HomeAccessory): self._flag = {CHAR_ON: False, CHAR_BRIGHTNESS: False, CHAR_HUE: False, CHAR_SATURATION: False, RGB_COLOR: False} + self._state = 0 self.chars = [] self._features = self._hass.states.get(self._entity_id) \ @@ -47,7 +48,7 @@ class Light(HomeAccessory): serv_light = add_preload_service(self, SERV_LIGHTBULB, self.chars) self.char_on = serv_light.get_characteristic(CHAR_ON) self.char_on.setter_callback = self.set_state - self.char_on.value = 0 + self.char_on.value = self._state if CHAR_BRIGHTNESS in self.chars: self.char_brightness = serv_light \ @@ -66,7 +67,7 @@ class Light(HomeAccessory): def set_state(self, value): """Set state if call came from HomeKit.""" - if self._flag[CHAR_BRIGHTNESS]: + if self._state == value: return _LOGGER.debug('%s: Set state to %d', self._entity_id, value) @@ -83,8 +84,11 @@ class Light(HomeAccessory): _LOGGER.debug('%s: Set brightness to %d', self._entity_id, value) self._flag[CHAR_BRIGHTNESS] = True self.char_brightness.set_value(value, should_callback=False) - self._hass.components.light.turn_on( - self._entity_id, brightness_pct=value) + if value != 0: + self._hass.components.light.turn_on( + self._entity_id, brightness_pct=value) + else: + self._hass.components.light.turn_off(self._entity_id) def set_saturation(self, value): """Set saturation if call came from HomeKit.""" @@ -121,10 +125,11 @@ class Light(HomeAccessory): # Handle State state = new_state.state - if not self._flag[CHAR_ON] and state in [STATE_ON, STATE_OFF] and \ - self.char_on.value != (state == STATE_ON): - self.char_on.set_value(state == STATE_ON, should_callback=False) - self._flag[CHAR_ON] = False + if state in (STATE_ON, STATE_OFF): + self._state = 1 if state == STATE_ON else 0 + if not self._flag[CHAR_ON] and self.char_on.value != self._state: + self.char_on.set_value(self._state, should_callback=False) + self._flag[CHAR_ON] = False # Handle Brightness if CHAR_BRIGHTNESS in self.chars: diff --git a/tests/components/homekit/test_type_lights.py b/tests/components/homekit/test_type_lights.py index b4d4d5a5945..ee1900fd7c5 100644 --- a/tests/components/homekit/test_type_lights.py +++ b/tests/components/homekit/test_type_lights.py @@ -57,19 +57,21 @@ class TestHomekitLights(unittest.TestCase): self.assertEqual(acc.char_on.value, 0) # Set from HomeKit - acc.char_on.set_value(True) + acc.char_on.set_value(1) self.hass.block_till_done() - self.assertEqual( - self.events[0].data[ATTR_DOMAIN], DOMAIN) - self.assertEqual( - self.events[0].data[ATTR_SERVICE], SERVICE_TURN_ON) + self.assertEqual(self.events[0].data[ATTR_DOMAIN], DOMAIN) + self.assertEqual(self.events[0].data[ATTR_SERVICE], SERVICE_TURN_ON) - acc.char_on.set_value(False) + self.hass.states.set(entity_id, STATE_ON) + self.hass.block_till_done() + + acc.char_on.set_value(0) + self.hass.block_till_done() + self.assertEqual(self.events[1].data[ATTR_DOMAIN], DOMAIN) + self.assertEqual(self.events[1].data[ATTR_SERVICE], SERVICE_TURN_OFF) + + self.hass.states.set(entity_id, STATE_OFF) self.hass.block_till_done() - self.assertEqual( - self.events[1].data[ATTR_DOMAIN], DOMAIN) - self.assertEqual( - self.events[1].data[ATTR_SERVICE], SERVICE_TURN_OFF) # Remove entity self.hass.states.remove(entity_id) @@ -95,15 +97,27 @@ class TestHomekitLights(unittest.TestCase): acc.char_brightness.set_value(20) acc.char_on.set_value(1) self.hass.block_till_done() - self.assertEqual( - self.events[0].data[ATTR_DOMAIN], DOMAIN) - self.assertEqual( - self.events[0].data[ATTR_SERVICE], SERVICE_TURN_ON) - print(self.events[0].data) + self.assertEqual(self.events[0].data[ATTR_DOMAIN], DOMAIN) + self.assertEqual(self.events[0].data[ATTR_SERVICE], SERVICE_TURN_ON) self.assertEqual( self.events[0].data[ATTR_SERVICE_DATA], { ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS_PCT: 20}) + acc.char_on.set_value(1) + acc.char_brightness.set_value(40) + self.hass.block_till_done() + self.assertEqual(self.events[1].data[ATTR_DOMAIN], DOMAIN) + self.assertEqual(self.events[1].data[ATTR_SERVICE], SERVICE_TURN_ON) + self.assertEqual( + self.events[1].data[ATTR_SERVICE_DATA], { + ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS_PCT: 40}) + + acc.char_on.set_value(1) + acc.char_brightness.set_value(0) + self.hass.block_till_done() + self.assertEqual(self.events[2].data[ATTR_DOMAIN], DOMAIN) + self.assertEqual(self.events[2].data[ATTR_SERVICE], SERVICE_TURN_OFF) + def test_light_rgb_color(self): """Test light with rgb_color.""" entity_id = 'light.demo' @@ -123,10 +137,8 @@ class TestHomekitLights(unittest.TestCase): acc.char_hue.set_value(145) acc.char_saturation.set_value(75) self.hass.block_till_done() - self.assertEqual( - self.events[0].data[ATTR_DOMAIN], DOMAIN) - self.assertEqual( - self.events[0].data[ATTR_SERVICE], SERVICE_TURN_ON) + self.assertEqual(self.events[0].data[ATTR_DOMAIN], DOMAIN) + self.assertEqual(self.events[0].data[ATTR_SERVICE], SERVICE_TURN_ON) self.assertEqual( self.events[0].data[ATTR_SERVICE_DATA], { ATTR_ENTITY_ID: entity_id, ATTR_HS_COLOR: (145, 75)}) From 867010240accca53ad706f9878484af17328f138 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Fri, 30 Mar 2018 02:12:11 +0200 Subject: [PATCH 214/220] Construct version pinned (#13528) * Construct added to the requirements * requirements_all.txt updated --- homeassistant/components/climate/eq3btsmart.py | 2 +- homeassistant/components/fan/xiaomi_miio.py | 2 +- homeassistant/components/light/xiaomi_miio.py | 2 +- homeassistant/components/remote/xiaomi_miio.py | 2 +- .../components/sensor/eddystone_temperature.py | 2 +- homeassistant/components/sensor/xiaomi_miio.py | 2 +- homeassistant/components/switch/xiaomi_miio.py | 2 +- homeassistant/components/vacuum/xiaomi_miio.py | 2 +- requirements_all.txt | 10 ++++++++++ 9 files changed, 18 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/climate/eq3btsmart.py b/homeassistant/components/climate/eq3btsmart.py index 5c0a3530006..820e715b00d 100644 --- a/homeassistant/components/climate/eq3btsmart.py +++ b/homeassistant/components/climate/eq3btsmart.py @@ -15,7 +15,7 @@ from homeassistant.const import ( CONF_MAC, CONF_DEVICES, TEMP_CELSIUS, ATTR_TEMPERATURE, PRECISION_HALVES) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['python-eq3bt==0.1.9'] +REQUIREMENTS = ['python-eq3bt==0.1.9', 'construct==2.9.41'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/fan/xiaomi_miio.py b/homeassistant/components/fan/xiaomi_miio.py index 3e6aee6ba3a..4df85711cfd 100644 --- a/homeassistant/components/fan/xiaomi_miio.py +++ b/homeassistant/components/fan/xiaomi_miio.py @@ -29,7 +29,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, }) -REQUIREMENTS = ['python-miio==0.3.9'] +REQUIREMENTS = ['python-miio==0.3.9', 'construct==2.9.41'] ATTR_TEMPERATURE = 'temperature' ATTR_HUMIDITY = 'humidity' diff --git a/homeassistant/components/light/xiaomi_miio.py b/homeassistant/components/light/xiaomi_miio.py index 999d0f7f0e6..21a27c33203 100644 --- a/homeassistant/components/light/xiaomi_miio.py +++ b/homeassistant/components/light/xiaomi_miio.py @@ -41,7 +41,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ 'philips.light.candle2']), }) -REQUIREMENTS = ['python-miio==0.3.9'] +REQUIREMENTS = ['python-miio==0.3.9', 'construct==2.9.41'] # The light does not accept cct values < 1 CCT_MIN = 1 diff --git a/homeassistant/components/remote/xiaomi_miio.py b/homeassistant/components/remote/xiaomi_miio.py index 13ec9c873b1..b71eb2cb447 100644 --- a/homeassistant/components/remote/xiaomi_miio.py +++ b/homeassistant/components/remote/xiaomi_miio.py @@ -22,7 +22,7 @@ from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv from homeassistant.util.dt import utcnow -REQUIREMENTS = ['python-miio==0.3.9'] +REQUIREMENTS = ['python-miio==0.3.9', 'construct==2.9.41'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sensor/eddystone_temperature.py b/homeassistant/components/sensor/eddystone_temperature.py index fb5fa2c1fba..06accb26eb6 100644 --- a/homeassistant/components/sensor/eddystone_temperature.py +++ b/homeassistant/components/sensor/eddystone_temperature.py @@ -18,7 +18,7 @@ from homeassistant.const import ( CONF_NAME, TEMP_CELSIUS, STATE_UNKNOWN, EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_START) -REQUIREMENTS = ['beacontools[scan]==1.2.1'] +REQUIREMENTS = ['beacontools[scan]==1.2.1', 'construct==2.9.41'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sensor/xiaomi_miio.py b/homeassistant/components/sensor/xiaomi_miio.py index 33ba5793fe0..066dc384007 100644 --- a/homeassistant/components/sensor/xiaomi_miio.py +++ b/homeassistant/components/sensor/xiaomi_miio.py @@ -25,7 +25,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, }) -REQUIREMENTS = ['python-miio==0.3.9'] +REQUIREMENTS = ['python-miio==0.3.9', 'construct==2.9.41'] ATTR_POWER = 'power' ATTR_CHARGING = 'charging' diff --git a/homeassistant/components/switch/xiaomi_miio.py b/homeassistant/components/switch/xiaomi_miio.py index 27c3c4c72f1..6110b6dc469 100644 --- a/homeassistant/components/switch/xiaomi_miio.py +++ b/homeassistant/components/switch/xiaomi_miio.py @@ -37,7 +37,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ 'chuangmi.plug.v2']), }) -REQUIREMENTS = ['python-miio==0.3.9'] +REQUIREMENTS = ['python-miio==0.3.9', 'construct==2.9.41'] ATTR_POWER = 'power' ATTR_TEMPERATURE = 'temperature' diff --git a/homeassistant/components/vacuum/xiaomi_miio.py b/homeassistant/components/vacuum/xiaomi_miio.py index 887a50fdcce..b2451ed495c 100644 --- a/homeassistant/components/vacuum/xiaomi_miio.py +++ b/homeassistant/components/vacuum/xiaomi_miio.py @@ -19,7 +19,7 @@ from homeassistant.const import ( ATTR_ENTITY_ID, CONF_HOST, CONF_NAME, CONF_TOKEN, STATE_OFF, STATE_ON) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['python-miio==0.3.9'] +REQUIREMENTS = ['python-miio==0.3.9', 'construct==2.9.41'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index a72d33e35ec..7ac9bd5fd7e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -189,6 +189,16 @@ colorlog==3.1.2 # homeassistant.components.binary_sensor.concord232 concord232==0.15 +# homeassistant.components.climate.eq3btsmart +# homeassistant.components.fan.xiaomi_miio +# homeassistant.components.light.xiaomi_miio +# homeassistant.components.remote.xiaomi_miio +# homeassistant.components.sensor.eddystone_temperature +# homeassistant.components.sensor.xiaomi_miio +# homeassistant.components.switch.xiaomi_miio +# homeassistant.components.vacuum.xiaomi_miio +construct==2.9.41 + # homeassistant.scripts.credstash # credstash==1.14.0 From 32b0712089e9c52a6b840dd1d177f96627d39d2b Mon Sep 17 00:00:00 2001 From: Johann Kellerman Date: Fri, 30 Mar 2018 02:13:08 +0200 Subject: [PATCH 215/220] Don't add Falsy items to list #13412 (#13536) --- homeassistant/config.py | 4 ++++ tests/test_config.py | 19 +++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/homeassistant/config.py b/homeassistant/config.py index 53e611ac725..28936ae12e9 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -554,6 +554,8 @@ def merge_packages_config(config, packages, _log_pkg_error=_log_pkg_error): continue if hasattr(component, 'PLATFORM_SCHEMA'): + if not comp_conf: + continue # Ensure we dont add Falsy items to list config[comp_name] = cv.ensure_list(config.get(comp_name)) config[comp_name].extend(cv.ensure_list(comp_conf)) continue @@ -562,6 +564,8 @@ def merge_packages_config(config, packages, _log_pkg_error=_log_pkg_error): merge_type, _ = _identify_config_schema(component) if merge_type == 'list': + if not comp_conf: + continue # Ensure we dont add Falsy items to list config[comp_name] = cv.ensure_list(config.get(comp_name)) config[comp_name].extend(cv.ensure_list(comp_conf)) continue diff --git a/tests/test_config.py b/tests/test_config.py index 22fcebc6ea4..652b931366a 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -592,6 +592,25 @@ def test_merge(merge_log_err): assert config['wake_on_lan'] is None +def test_merge_try_falsy(merge_log_err): + """Ensure we dont add falsy items like empty OrderedDict() to list.""" + packages = { + 'pack_falsy_to_lst': {'automation': OrderedDict()}, + 'pack_list2': {'light': OrderedDict()}, + } + config = { + config_util.CONF_CORE: {config_util.CONF_PACKAGES: packages}, + 'automation': {'do': 'something'}, + 'light': {'some': 'light'}, + } + config_util.merge_packages_config(config, packages) + + assert merge_log_err.call_count == 0 + assert len(config) == 3 + assert len(config['automation']) == 1 + assert len(config['light']) == 1 + + def test_merge_new(merge_log_err): """Test adding new components to outer scope.""" packages = { From f26aff48859ed11a04acd22dfeaf34741d6f5aa2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 29 Mar 2018 17:21:48 -0700 Subject: [PATCH 216/220] Version bump to 0.66.0b3 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 58c49289989..ccb75634601 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 66 -PATCH_VERSION = '0.b2' +PATCH_VERSION = '0b3' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From 78f3e01854d82c696578fbeb733902997864f81d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 30 Mar 2018 00:23:02 -0700 Subject: [PATCH 217/220] Fix version bump script --- script/version_bump.py | 157 ++++++++++++++++++++++++----------------- 1 file changed, 91 insertions(+), 66 deletions(-) diff --git a/script/version_bump.py b/script/version_bump.py index 0500fc45957..59060a7075b 100755 --- a/script/version_bump.py +++ b/script/version_bump.py @@ -3,82 +3,97 @@ import argparse import re +from packaging.version import Version + from homeassistant import const -PARSE_PATCH = r'(?P\d+)(\.(?P\D+)(?P\d+))?' +def _bump_release(release, bump_type): + """Bump a release tuple consisting of 3 numbers.""" + major, minor, patch = release + + if bump_type == 'patch': + patch += 1 + elif bump_type == 'minor': + minor += 1 + patch = 0 + + return major, minor, patch -def format_patch(patch_parts): - """Format the patch parts back into a patch string.""" - return '{patch}.{prerel}{prerelversion}'.format(**patch_parts) - - -def bump_version(cur_major, cur_minor, cur_patch, bump_type): +def bump_version(version, bump_type): """Return a new version given a current version and action.""" - patch_parts = re.match(PARSE_PATCH, cur_patch).groupdict() - patch_parts['patch'] = int(patch_parts['patch']) - if patch_parts['prerelversion'] is not None: - patch_parts['prerelversion'] = int(patch_parts['prerelversion']) + to_change = {} - if bump_type == 'release_patch': + if bump_type == 'minor': + # Convert 0.67.3 to 0.68.0 + # Convert 0.67.3.b5 to 0.68.0 + # Convert 0.67.3.dev0 to 0.68.0 + # Convert 0.67.0.b5 to 0.67.0 + # Convert 0.67.0.dev0 to 0.67.0 + to_change['dev'] = None + to_change['pre'] = None + + if not version.is_prerelease or version.release[2] != 0: + to_change['release'] = _bump_release(version.release, 'minor') + + elif bump_type == 'patch': # Convert 0.67.3 to 0.67.4 # Convert 0.67.3.b5 to 0.67.3 # Convert 0.67.3.dev0 to 0.67.3 - new_major = cur_major - new_minor = cur_minor + to_change['dev'] = None + to_change['pre'] = None - if patch_parts['prerel'] is None: - new_patch = str(patch_parts['patch'] + 1) - else: - new_patch = str(patch_parts['patch']) + if not version.is_prerelease: + to_change['release'] = _bump_release(version.release, 'patch') elif bump_type == 'dev': # Convert 0.67.3 to 0.67.4.dev0 # Convert 0.67.3.b5 to 0.67.4.dev0 # Convert 0.67.3.dev0 to 0.67.3.dev1 - new_major = cur_major - - if patch_parts['prerel'] == 'dev': - new_minor = cur_minor - patch_parts['prerelversion'] += 1 - new_patch = format_patch(patch_parts) + if version.is_devrelease: + to_change['dev'] = ('dev', version.dev + 1) else: - new_minor = cur_minor + 1 - new_patch = '0.dev0' + to_change['pre'] = ('dev', 0) + to_change['release'] = _bump_release(version.release, 'minor') elif bump_type == 'beta': - # Convert 0.67.5 to 0.67.8.b0 - # Convert 0.67.0.dev0 to 0.67.0.b0 - # Convert 0.67.5.b4 to 0.67.5.b5 - new_major = cur_major - new_minor = cur_minor + # Convert 0.67.5 to 0.67.6b0 + # Convert 0.67.0.dev0 to 0.67.0b0 + # Convert 0.67.5.b4 to 0.67.5b5 - if patch_parts['prerel'] is None: - patch_parts['patch'] += 1 - patch_parts['prerel'] = 'b' - patch_parts['prerelversion'] = 0 + if version.is_devrelease: + to_change['dev'] = None + to_change['pre'] = ('b', 0) - elif patch_parts['prerel'] == 'b': - patch_parts['prerelversion'] += 1 - - elif patch_parts['prerel'] == 'dev': - patch_parts['prerel'] = 'b' - patch_parts['prerelversion'] = 0 + elif version.is_prerelease: + if version.pre[0] == 'a': + to_change['pre'] = ('b', 0) + if version.pre[0] == 'b': + to_change['pre'] = ('b', version.pre[1] + 1) + else: + to_change['pre'] = ('b', 0) + to_change['release'] = _bump_release(version.release, 'patch') else: - raise Exception('Can only bump from beta or no prerel version') + to_change['release'] = _bump_release(version.release, 'patch') + to_change['pre'] = ('b', 0) - new_patch = format_patch(patch_parts) + else: + assert False, 'Unsupported type: {}'.format(bump_type) - return new_major, new_minor, new_patch + temp = Version('0') + temp._version = version._version._replace(**to_change) + return Version(str(temp)) -def write_version(major, minor, patch): +def write_version(version): """Update Home Assistant constant file with new version.""" with open('homeassistant/const.py') as fil: content = fil.read() + major, minor, patch = str(version).split('.', 2) + content = re.sub('MAJOR_VERSION = .*\n', 'MAJOR_VERSION = {}\n'.format(major), content) @@ -100,35 +115,45 @@ def main(): parser.add_argument( 'type', help="The type of the bump the version to.", - choices=['beta', 'dev', 'release_patch'], + choices=['beta', 'dev', 'patch', 'minor'], ) arguments = parser.parse_args() - write_version(*bump_version(const.MAJOR_VERSION, const.MINOR_VERSION, - const.PATCH_VERSION, arguments.type)) + current = Version(const.__version__) + bumped = bump_version(current, arguments.type) + assert bumped > current, 'BUG! New version is not newer than old version' + write_version(bumped) def test_bump_version(): """Make sure it all works.""" - assert bump_version(0, 56, '0', 'beta') == \ - (0, 56, '1.b0') - assert bump_version(0, 56, '0.b3', 'beta') == \ - (0, 56, '0.b4') - assert bump_version(0, 56, '0.dev0', 'beta') == \ - (0, 56, '0.b0') + assert bump_version(Version('0.56.0'), 'beta') == Version('0.56.1b0') + assert bump_version(Version('0.56.0b3'), 'beta') == Version('0.56.0b4') + assert bump_version(Version('0.56.0.dev0'), 'beta') == Version('0.56.0b0') - assert bump_version(0, 56, '3', 'dev') == \ - (0, 57, '0.dev0') - assert bump_version(0, 56, '0.b3', 'dev') == \ - (0, 57, '0.dev0') - assert bump_version(0, 56, '0.dev0', 'dev') == \ - (0, 56, '0.dev1') + assert bump_version(Version('0.56.3'), 'dev') == Version('0.57.0.dev0') + assert bump_version(Version('0.56.0b3'), 'dev') == Version('0.57.0.dev0') + assert bump_version(Version('0.56.0.dev0'), 'dev') == \ + Version('0.56.0.dev1') - assert bump_version(0, 56, '3', 'release_patch') == \ - (0, 56, '4') - assert bump_version(0, 56, '3.b3', 'release_patch') == \ - (0, 56, '3') - assert bump_version(0, 56, '0.dev0', 'release_patch') == \ - (0, 56, '0') + assert bump_version(Version('0.56.3'), 'patch') == \ + Version('0.56.4') + assert bump_version(Version('0.56.3.b3'), 'patch') == \ + Version('0.56.3') + assert bump_version(Version('0.56.0.dev0'), 'patch') == \ + Version('0.56.0') + + assert bump_version(Version('0.56.0'), 'minor') == \ + Version('0.57.0') + assert bump_version(Version('0.56.3'), 'minor') == \ + Version('0.57.0') + assert bump_version(Version('0.56.0.b3'), 'minor') == \ + Version('0.56.0') + assert bump_version(Version('0.56.3.b3'), 'minor') == \ + Version('0.57.0') + assert bump_version(Version('0.56.0.dev0'), 'minor') == \ + Version('0.56.0') + assert bump_version(Version('0.56.2.dev0'), 'minor') == \ + Version('0.57.0') if __name__ == '__main__': From 9fc8a8f67900a3db5be22e9d5a589a37db46b823 Mon Sep 17 00:00:00 2001 From: Johann Kellerman Date: Fri, 30 Mar 2018 04:57:19 +0200 Subject: [PATCH 218/220] Check whitelisted paths #13107 (#13154) --- homeassistant/core.py | 10 +++++++--- tests/test_core.py | 3 ++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/homeassistant/core.py b/homeassistant/core.py index 65db82a1fbe..feb8d331ae8 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -1060,15 +1060,19 @@ class Config(object): """Check if the path is valid for access from outside.""" assert path is not None - parent = pathlib.Path(path) + thepath = pathlib.Path(path) try: - parent = parent.resolve() # pylint: disable=no-member + # The file path does not have to exist (it's parent should) + if thepath.exists(): + thepath = thepath.resolve() + else: + thepath = thepath.parent.resolve() except (FileNotFoundError, RuntimeError, PermissionError): return False for whitelisted_path in self.whitelist_external_dirs: try: - parent.relative_to(whitelisted_path) + thepath.relative_to(whitelisted_path) return True except ValueError: pass diff --git a/tests/test_core.py b/tests/test_core.py index 7a1610c0966..1fcd9416f36 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -809,7 +809,8 @@ class TestConfig(unittest.TestCase): valid = [ test_file, - tmp_dir + tmp_dir, + os.path.join(tmp_dir, 'notfound321') ] for path in valid: assert self.config.is_allowed_path(path) From 0f2cfe7f2760a32ed287e1de0d9fe30f2e3e6b7e Mon Sep 17 00:00:00 2001 From: dramamoose Date: Fri, 30 Mar 2018 15:10:25 -0600 Subject: [PATCH 219/220] Fix FLUX_LED error when no color is set (#13527) * Handle turn_on situation when no color is set As is, an error gets thrown when turn_on is called without an HS value. By adding an if statement, we only try to set RGB if an HS value is applied. * Fix Whitespace Issues * Made Requested Changes --- homeassistant/components/light/flux_led.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/light/flux_led.py b/homeassistant/components/light/flux_led.py index ed0836f1449..6ffdcc0bb4a 100644 --- a/homeassistant/components/light/flux_led.py +++ b/homeassistant/components/light/flux_led.py @@ -204,7 +204,12 @@ class FluxLight(Light): self._bulb.turnOn() hs_color = kwargs.get(ATTR_HS_COLOR) - rgb = color_util.color_hs_to_RGB(*hs_color) + + if hs_color: + rgb = color_util.color_hs_to_RGB(*hs_color) + else: + rgb = None + brightness = kwargs.get(ATTR_BRIGHTNESS) effect = kwargs.get(ATTR_EFFECT) From 4dea55b29c665955d27d4d1913589d51fae62ca9 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 30 Mar 2018 14:11:32 -0700 Subject: [PATCH 220/220] Version bump to 0.66.0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index ccb75634601..3dce8882015 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 66 -PATCH_VERSION = '0b3' +PATCH_VERSION = '0' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3)